Merge pull request #1008 from wabbajack-tools/mirrored-files

Mirrored files
This commit is contained in:
Timothy Baldridge 2020-08-05 18:18:04 -07:00 committed by GitHub
commit e14501afdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 526 additions and 40 deletions

View File

@ -26,7 +26,9 @@ namespace Wabbajack.CLI
typeof(InlinedFileReport), typeof(InlinedFileReport),
typeof(ExtractBSA), typeof(ExtractBSA),
typeof(PurgeNexusCache), typeof(PurgeNexusCache),
typeof(ForceHealing) typeof(ForceHealing),
typeof(HashVariants),
typeof(ParseMeta)
}; };
} }
} }

View File

@ -0,0 +1,24 @@
using System;
using System.Threading.Tasks;
using CommandLine;
using Wabbajack.Common;
namespace Wabbajack.CLI.Verbs
{
[Verb("hash-variants", HelpText = "Print all the known variants (formats) of a hash")]
public class HashVariants : AVerb
{
[Option('i', "input", Required = true, HelpText = "Input Hash")]
public string Input { get; set; } = "";
protected override async Task<ExitCode> Run()
{
var hash = Hash.Interpret(Input);
Console.WriteLine($"Base64: {hash.ToBase64()}");
Console.WriteLine($"Hex: {hash.ToHex()}");
Console.WriteLine($"Long: {(long)hash}");
Console.WriteLine($"ULong (uncommon): {(ulong)hash}");
return ExitCode.Ok;
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Threading.Tasks;
using CommandLine;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack.CLI.Verbs
{
[Verb("parse-meta", HelpText = "Parse a .meta file, figure out the download state and print it")]
public class ParseMeta : AVerb
{
[Option('i', "input", Required = true, HelpText = "Input meta file to parse")]
public string Input { get; set; } = "";
protected override async Task<ExitCode> Run()
{
var meta = (AbstractDownloadState)await DownloadDispatcher.ResolveArchive(((AbsolutePath)Input).LoadIniFile());
if (meta == null)
{
Console.WriteLine("Cannot resolve meta!");
return ExitCode.Error;
}
Console.WriteLine($"PrimaryKeyString : {meta.PrimaryKeyString}");
return ExitCode.Ok;
}
}
}

View File

@ -148,7 +148,7 @@ namespace Wabbajack.Common
public static long UPLOADED_FILE_BLOCK_SIZE = (long)1024 * 1024 * 2; public static long UPLOADED_FILE_BLOCK_SIZE = (long)1024 * 1024 * 2;
public static string ArchiveUpdatesCDNFolder = "archive_updates"; public static Uri WabbajackMirror = new Uri("https://wabbajack-mirror.b-cdn.net");
} }
} }

View File

@ -103,6 +103,16 @@ namespace Wabbajack.Common
{ {
return BitConverter.GetBytes(_code); return BitConverter.GetBytes(_code);
} }
public static Hash Interpret(string input)
{
return input.Length switch
{
16 => FromHex(input),
12 when input.EndsWith('=') => FromBase64(input),
_ => FromLong(long.Parse(input))
};
}
} }
public static partial class Utils public static partial class Utils

View File

@ -150,6 +150,9 @@ namespace Wabbajack.Lib
await using (var of = await ModListOutputFolder.Combine("modlist").Create()) await using (var of = await ModListOutputFolder.Combine("modlist").Create())
ModList.ToJson(of); ModList.ToJson(of);
await ModListOutputFolder.Combine("sig")
.WriteAllBytesAsync((await ModListOutputFolder.Combine("modlist").FileHashAsync()).ToArray());
await ClientAPI.SendModListDefinition(ModList); await ClientAPI.SendModListDefinition(ModList);
await ModListOutputFile.DeleteAsync(); await ModListOutputFile.DeleteAsync();
@ -157,6 +160,7 @@ namespace Wabbajack.Lib
await using (var fs = await ModListOutputFile.Create()) await using (var fs = await ModListOutputFile.Create())
{ {
using var za = new ZipArchive(fs, ZipArchiveMode.Create); using var za = new ZipArchive(fs, ZipArchiveMode.Create);
await ModListOutputFolder.EnumerateFiles() await ModListOutputFolder.EnumerateFiles()
.DoProgress("Compressing ModList", .DoProgress("Compressing ModList",
async f => async f =>

View File

@ -39,7 +39,7 @@ namespace Wabbajack.Lib.AuthorApi
} }
public async Task<CDNFileDefinition> GenerateFileDefinition(WorkQueue queue, AbsolutePath path, Action<string, Percent> progressFn) public static async Task<CDNFileDefinition> GenerateFileDefinition(WorkQueue queue, AbsolutePath path, Action<string, Percent> progressFn)
{ {
IEnumerable<CDNFilePartDefinition> Blocks(AbsolutePath path) IEnumerable<CDNFilePartDefinition> Blocks(AbsolutePath path)
{ {

View File

@ -93,8 +93,14 @@ namespace Wabbajack.Lib.Downloaders
public static async Task<bool> DownloadWithPossibleUpgrade(Archive archive, AbsolutePath destination) public static async Task<bool> DownloadWithPossibleUpgrade(Archive archive, AbsolutePath destination)
{ {
var success = await Download(archive, destination); if (await Download(archive, destination))
if (success) {
await destination.FileHashCachedAsync();
return true;
}
if (await DownloadFromMirror(archive, destination))
{ {
await destination.FileHashCachedAsync(); await destination.FileHashCachedAsync();
return true; return true;
@ -147,6 +153,24 @@ namespace Wabbajack.Lib.Downloaders
return true; return true;
} }
private static async Task<bool> DownloadFromMirror(Archive archive, AbsolutePath destination)
{
try
{
var newArchive =
new Archive(
new WabbajackCDNDownloader.State(new Uri($"{Consts.WabbajackMirror}{archive.Hash.ToHex()}")))
{
Hash = archive.Hash, Size = archive.Size, Name = archive.Name
};
return await Download(newArchive, destination);
}
catch (Exception ex)
{
return false;
}
}
private static async Task<bool> Download(Archive archive, AbsolutePath destination) private static async Task<bool> Download(Archive archive, AbsolutePath destination)
{ {
try try

View File

@ -61,6 +61,7 @@ namespace Wabbajack.BuildServer.Test
_token = new CancellationTokenSource(); _token = new CancellationTokenSource();
_task = _host.RunAsync(_token.Token); _task = _host.RunAsync(_token.Token);
Consts.WabbajackBuildServerUri = new Uri("http://localhost:8080"); Consts.WabbajackBuildServerUri = new Uri("http://localhost:8080");
Consts.WabbajackMirror = new Uri("https://wabbajack-test.b-cdn.net");
await "ServerWhitelist.yaml".RelativeTo(ServerPublicFolder).WriteAllTextAsync( await "ServerWhitelist.yaml".RelativeTo(ServerPublicFolder).WriteAllTextAsync(
"GoogleIDs:\nAllowedPrefixes:\n - http://localhost"); "GoogleIDs:\nAllowedPrefixes:\n - http://localhost");

View File

@ -0,0 +1,55 @@
using System;
using System.Threading.Tasks;
using Wabbajack.BuildServer.Test;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Server.DataLayer;
using Wabbajack.Server.DTOs;
using Wabbajack.Server.Services;
using Xunit;
using Xunit.Abstractions;
namespace Wabbajack.Server.Test
{
public class MirroredFilesTests : ABuildServerSystemTest
{
public MirroredFilesTests(ITestOutputHelper output, SingletonAdaptor<BuildServerFixture> fixture) : base(output, fixture)
{
}
[Fact]
public async Task CanUploadAndDownloadMirroredFiles()
{
var file = new TempFile();
await file.Path.WriteAllBytesAsync(RandomData(1024 * 1024 * 6));
var dataHash = await file.Path.FileHashAsync();
await Fixture.GetService<ArchiveMaintainer>().Ingest(file.Path);
Assert.True(Fixture.GetService<ArchiveMaintainer>().HaveArchive(dataHash));
var sql = Fixture.GetService<SqlService>();
await sql.UpsertMirroredFile(new MirroredFile
{
Created = DateTime.UtcNow,
Rationale = "Test File",
Hash = dataHash
});
var uploader = Fixture.GetService<MirrorUploader>();
Assert.Equal(1, await uploader.Execute());
var archive = new Archive(new HTTPDownloader.State(MakeURL(dataHash.ToString())))
{
Hash = dataHash,
Size = file.Path.Size
};
var file2 = new TempFile();
await DownloadDispatcher.DownloadWithPossibleUpgrade(archive, file2.Path);
}
}
}

View File

@ -70,7 +70,7 @@ namespace Wabbajack.Server.Test
await Assert.ThrowsAsync<HttpException>(async () => await ClientAPI.GetModUpgrade(oldArchive, newArchive, TimeSpan.Zero, TimeSpan.Zero)); await Assert.ThrowsAsync<HttpException>(async () => await ClientAPI.GetModUpgrade(oldArchive, newArchive, TimeSpan.Zero, TimeSpan.Zero));
Assert.True(await patcher.Execute() > 1); Assert.True(await patcher.Execute() > 1);
Assert.Equal(new Uri("https://wabbajacktest.b-cdn.net/archive_updates/79223277e28e1b7b_3286c571d95f5666"),await ClientAPI.GetModUpgrade(oldArchive, newArchive, TimeSpan.Zero, TimeSpan.Zero)); Assert.Equal(new Uri("https://wabbajacktest.b-cdn.net/79223277e28e1b7b_3286c571d95f5666"),await ClientAPI.GetModUpgrade(oldArchive, newArchive, TimeSpan.Zero, TimeSpan.Zero));
} }
[Fact] [Fact]

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>

View File

@ -669,6 +669,50 @@ CONSTRAINT [PK_NexusModPermissions] PRIMARY KEY CLUSTERED
) ON [PRIMARY] ) ON [PRIMARY]
GO GO
/****** Object: Table [dbo].[MirroredArchives] Script Date: 8/3/2020 8:39:33 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[MirroredArchives](
[Hash] [bigint] NOT NULL,
[Created] [datetime] NOT NULL,
[Uploaded] [datetime] NULL,
[Rationale] [nvarchar](max) NOT NULL,
[FailMessage] [nvarchar](max) NULL,
CONSTRAINT [PK_MirroredArchives] PRIMARY KEY CLUSTERED
(
[Hash] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
/****** Object: Table [dbo].[GameMetadata] Script Date: 8/3/2020 8:39:33 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[GameMetadata](
[NexusGameId] [bigint] NULL,
[WabbajackName] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_GameMetadata] PRIMARY KEY CLUSTERED
(
[WabbajackName] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
)
GO
CREATE NONCLUSTERED INDEX [IDX_GameAndName-20200804-164236] ON [dbo].[GameMetadata]
(
[NexusGameId] ASC,
[WabbajackName] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
/****** Object: StoredProcedure [dbo].[MergeAllFilesInArchive] Script Date: 3/28/2020 4:58:59 PM ******/ /****** Object: StoredProcedure [dbo].[MergeAllFilesInArchive] Script Date: 3/28/2020 4:58:59 PM ******/

View File

@ -106,7 +106,7 @@ namespace Wabbajack.BuildServer.Controllers
private async Task<FtpClient> GetBunnyCdnFtpClient() private async Task<FtpClient> GetBunnyCdnFtpClient()
{ {
var info = await Utils.FromEncryptedJson<BunnyCdnFtpInfo>("bunny-cdn-ftp-info"); var info = await BunnyCdnFtpInfo.GetCreds(StorageSpace.AuthoredFiles);
var client = new FtpClient(info.Hostname) {Credentials = new NetworkCredential(info.Username, info.Password)}; var client = new FtpClient(info.Hostname) {Credentials = new NetworkCredential(info.Username, info.Password)};
await client.ConnectAsync(); await client.ConnectAsync();
return client; return client;

View File

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib; using Wabbajack.Lib;
using Wabbajack.Server.DataLayer; using Wabbajack.Server.DataLayer;
using Wabbajack.Server.DTOs;
using Wabbajack.Server.Services; using Wabbajack.Server.Services;
namespace Wabbajack.BuildServer.Controllers namespace Wabbajack.BuildServer.Controllers
@ -20,6 +21,7 @@ namespace Wabbajack.BuildServer.Controllers
private DiscordWebHook _discord; private DiscordWebHook _discord;
private AppSettings _settings; private AppSettings _settings;
private QuickSync _quickSync; private QuickSync _quickSync;
private Task<BunnyCdnFtpInfo> _creds;
public ModUpgrade(ILogger<ModUpgrade> logger, SqlService sql, DiscordWebHook discord, QuickSync quickSync, AppSettings settings) public ModUpgrade(ILogger<ModUpgrade> logger, SqlService sql, DiscordWebHook discord, QuickSync quickSync, AppSettings settings)
{ {
@ -28,6 +30,7 @@ namespace Wabbajack.BuildServer.Controllers
_discord = discord; _discord = discord;
_settings = settings; _settings = settings;
_quickSync = quickSync; _quickSync = quickSync;
_creds = BunnyCdnFtpInfo.GetCreds(StorageSpace.Patches);
} }
[HttpPost] [HttpPost]
@ -93,7 +96,7 @@ namespace Wabbajack.BuildServer.Controllers
await _sql.MarkPatchUsage(oldDownload.Id, newDownload.Id); await _sql.MarkPatchUsage(oldDownload.Id, newDownload.Id);
return return
Ok( Ok(
$"https://{_settings.BunnyCDN_StorageZone}.b-cdn.net/{Consts.ArchiveUpdatesCDNFolder}/{request.OldArchive.Hash.ToHex()}_{request.NewArchive.Hash.ToHex()}"); $"https://{(await _creds).Username}.b-cdn.net/{request.OldArchive.Hash.ToHex()}_{request.NewArchive.Hash.ToHex()}");
} }
_logger.Log(LogLevel.Information, $"Upgrade requested from {oldDownload.Archive.Hash} to {newDownload.Archive.Hash} patch found but was failed"); _logger.Log(LogLevel.Information, $"Upgrade requested from {oldDownload.Archive.Hash} to {newDownload.Archive.Hash} patch found but was failed");

View File

@ -1,9 +1,25 @@
namespace Wabbajack.Server.DTOs using System.Collections.Generic;
using System.Threading.Tasks;
using Wabbajack.Common;
namespace Wabbajack.Server.DTOs
{ {
public enum StorageSpace
{
AuthoredFiles,
Patches,
Mirrors
}
public class BunnyCdnFtpInfo public class BunnyCdnFtpInfo
{ {
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }
public string Hostname { get; set; } public string Hostname { get; set; }
public static async Task<BunnyCdnFtpInfo> GetCreds(StorageSpace space)
{
return (await Utils.FromEncryptedJson<Dictionary<string, BunnyCdnFtpInfo>>("bunnycdn"))[space.ToString()];
}
} }
} }

View File

@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Server.DataLayer;
namespace Wabbajack.Server.DTOs
{
public class MirroredFile
{
public Hash Hash { get; set; }
public DateTime Created { get; set; }
public DateTime? Uploaded { get; set; }
public string Rationale { get; set; }
public string FailMessage { get; set; }
public async Task Finish(SqlService sql)
{
Uploaded = DateTime.UtcNow;
await sql.UpsertMirroredFile(this);
}
public async Task Fail(SqlService sql, string message)
{
Uploaded = DateTime.UtcNow;
FailMessage = message;
await sql.UpsertMirroredFile(this);
}
}
}

View File

@ -57,6 +57,12 @@ namespace Wabbajack.Server.DataLayer
return (await conn.QueryAsync<(Hash, string)>("SELECT Hash, PrimaryKeyString FROM ArchiveDownloads")).ToHashSet(); return (await conn.QueryAsync<(Hash, string)>("SELECT Hash, PrimaryKeyString FROM ArchiveDownloads")).ToHashSet();
} }
public async Task<HashSet<(Hash Hash, AbstractDownloadState State)>> GetAllArchiveDownloadStates()
{
await using var conn = await Open();
return (await conn.QueryAsync<(Hash, AbstractDownloadState)>("SELECT Hash, DownloadState FROM ArchiveDownloads")).ToHashSet();
}
public async Task<ArchiveDownload> GetArchiveDownload(Guid id) public async Task<ArchiveDownload> GetArchiveDownload(Guid id)
{ {

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Common;
using Dapper;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.NexusApi;
using Wabbajack.Server.DTOs;
namespace Wabbajack.Server.DataLayer
{
public partial class SqlService
{
public async Task<MirroredFile> GetNextMirroredFile()
{
await using var conn = await Open();
var result = await conn.QueryFirstOrDefaultAsync<(Hash, DateTime, DateTime, string, string)>(
"SELECT Hash, Created, Uploaded, Rationale, FailMessage from dbo.MirroredArchives WHERE Uploaded IS NULL");
if (result == default) return null;
return new MirroredFile
{
Hash = result.Item1, Created = result.Item2, Uploaded = result.Item3, Rationale = result.Item4, FailMessage = result.Item5
};
}
public async Task<HashSet<Hash>> GetAllMirroredHashes()
{
await using var conn = await Open();
return (await conn.QueryAsync<Hash>("SELECT Hash FROM dbo.MirroredArchives")).ToHashSet();
}
public async Task UpsertMirroredFile(MirroredFile file)
{
await using var conn = await Open();
await using var trans = await conn.BeginTransactionAsync();
await conn.ExecuteAsync("DELETE FROM dbo.MirroredArchives WHERE Hash = @Hash", new {file.Hash}, trans);
await conn.ExecuteAsync(
"INSERT INTO dbo.MirroredArchives (Hash, Created, Uploaded, Rationale, FailMessage) VALUES (@Hash, @Created, @Uploaded, @Rationale, @FailMessage)",
new
{
Hash = file.Hash,
Created = file.Created,
Uploaded = file.Uploaded,
Rationale = file.Rationale,
FailMessage = file.FailMessage
}, trans);
await trans.CommitAsync();
}
public async Task InsertAllNexusMirrors()
{
var permissions = (await GetNexusPermissions()).Where(p => p.Value == HTMLInterface.PermissionValue.Yes);
var downloads = (await GetAllArchiveDownloadStates()).Where(a => a.State is NexusDownloader.State).ToDictionary(a =>
{
var nd = (NexusDownloader.State)a.State;
return (nd.Game, nd.ModID);
}, a => a.Hash);
var existing = await GetAllMirroredHashes();
foreach (var (key, _) in permissions)
{
if (!downloads.TryGetValue(key, out var hash)) continue;
if (existing.Contains(hash)) continue;
await UpsertMirroredFile(new MirroredFile
{
Hash = hash,
Created = DateTime.UtcNow,
Rationale =
$"Mod ({key.Item1} {key.Item2}) has allowed re-upload permissions on the Nexus"
});
}
}
}
}

View File

@ -146,5 +146,38 @@ namespace Wabbajack.Server.DataLayer
await tx.CommitAsync(); await tx.CommitAsync();
} }
public async Task UpdateGameMetadata()
{
await using var conn = await Open();
var existing = (await conn.QueryAsync<string>("SELECT WabbajackName FROM dbo.GameMetadata")).ToHashSet();
var missing = GameRegistry.Games.Values.Where(g => !existing.Contains(g.Game.ToString())).ToList();
foreach (var add in missing.Where(g => g.NexusGameId != 0))
{
await conn.ExecuteAsync(
"INSERT INTO dbo.GameMetaData (NexusGameID, WabbajackName) VALUES (@NexusGameId, @WabbajackName)",
new {NexusGameId = add.NexusGameId, WabbajackName = add.Game.ToString()});
}
}
public async Task SetNexusPermission(Game game, long modId, HTMLInterface.PermissionValue perm)
{
await using var conn = await Open();
var tx = await conn.BeginTransactionAsync();
await conn.ExecuteAsync("DELETE FROM NexusModPermissions WHERE GameID = @GameID AND ModID = @ModID", new
{
GameID = game.MetaData().NexusGameId,
ModID = modId
},
transaction:tx);
await conn.ExecuteAsync(
"INSERT INTO NexusModPermissions (NexusGameID, ModID, Permissions) VALUES (@NexusGameID, @ModID, @Permissions)",
new {NexusGameID = game.MetaData().NexusGameId, ModID = modId, Permissions = (int)perm}, tx);
await tx.CommitAsync();
}
} }
} }

View File

@ -17,6 +17,7 @@ namespace Wabbajack.Server.Services
private TimeSpan _delay; private TimeSpan _delay;
protected ILogger<TP> _logger; protected ILogger<TP> _logger;
protected QuickSync _quickSync; protected QuickSync _quickSync;
private bool _isSetup;
public AbstractService(ILogger<TP> logger, AppSettings settings, QuickSync quickSync, TimeSpan delay) public AbstractService(ILogger<TP> logger, AppSettings settings, QuickSync quickSync, TimeSpan delay)
{ {
@ -24,19 +25,32 @@ namespace Wabbajack.Server.Services
_delay = delay; _delay = delay;
_logger = logger; _logger = logger;
_quickSync = quickSync; _quickSync = quickSync;
_isSetup = false;
}
public virtual async Task Setup()
{
} }
public void Start() public void Start()
{ {
if (_settings.RunBackEndJobs) if (_settings.RunBackEndJobs)
{ {
Task.Run(async () => Task.Run(async () =>
{ {
await Setup();
_isSetup = true;
while (true) while (true)
{ {
await _quickSync.ResetToken<TP>(); await _quickSync.ResetToken<TP>();
try try
{ {
_logger.LogInformation($"Running: {GetType().Name}");
await Execute(); await Execute();
} }
catch (Exception ex) catch (Exception ex)

View File

@ -0,0 +1,111 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Threading.Tasks;
using FluentFTP;
using Microsoft.Extensions.Logging;
using Wabbajack.BuildServer;
using Wabbajack.BuildServer.Controllers;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.AuthorApi;
using Wabbajack.Lib.FileUploader;
using Wabbajack.Server.DataLayer;
using Wabbajack.Server.DTOs;
namespace Wabbajack.Server.Services
{
public class MirrorUploader : AbstractService<MirrorUploader, int>
{
private SqlService _sql;
private ArchiveMaintainer _archives;
public MirrorUploader(ILogger<MirrorUploader> logger, AppSettings settings, SqlService sql, QuickSync quickSync, ArchiveMaintainer archives) : base(logger, settings, quickSync, TimeSpan.FromHours(1))
{
_sql = sql;
_archives = archives;
}
public override async Task<int> Execute()
{
int uploaded = 0;
TOP:
var toUpload = await _sql.GetNextMirroredFile();
if (toUpload == default) return uploaded;
uploaded += 1;
try
{
using var queue = new WorkQueue();
if (_archives.TryGetPath(toUpload.Hash, out var path))
{
_logger.LogInformation($"Uploading mirror file {toUpload.Hash} {path.Size.FileSizeToString()}");
var definition = await Client.GenerateFileDefinition(queue, path, (s, percent) => { });
var creds = await BunnyCdnFtpInfo.GetCreds(StorageSpace.Mirrors);
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}";
}
await definition.Parts.PMap(queue, async part =>
{
_logger.LogInformation($"Uploading mirror part ({part.Index}/{definition.Parts.Length})");
var name = MakePath(part.Index);
var buffer = new byte[part.Size];
await using (var fs = await path.OpenShared())
{
fs.Position = part.Offset;
await fs.ReadAsync(buffer);
}
using var client = await GetClient(creds);
await client.UploadAsync(new MemoryStream(buffer), name);
});
using (var client = await GetClient(creds))
{
_logger.LogInformation($"Finishing mirror upload");
await using var ms = new MemoryStream();
await using (var gz = new GZipStream(ms, CompressionLevel.Optimal, true))
{
definition.ToJson(gz);
}
ms.Position = 0;
await client.UploadAsync(ms, $"{definition.Hash.ToHex()}/definition.json.gz");
}
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;
}
private static async Task<FtpClient> GetClient(BunnyCdnFtpInfo creds)
{
var ftpClient = new FtpClient(creds.Hostname, new NetworkCredential(creds.Username, creds.Password));
await ftpClient.ConnectAsync();
return ftpClient;
}
}
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.BuildServer; using Wabbajack.BuildServer;
@ -16,7 +17,9 @@ namespace Wabbajack.Server.Services
private DiscordWebHook _discord; private DiscordWebHook _discord;
private SqlService _sql; private SqlService _sql;
public NexusPermissionsUpdater(ILogger<NexusKeyMaintainance> logger, AppSettings settings, QuickSync quickSync, DiscordWebHook discord, SqlService sql) : base(logger, settings, quickSync, TimeSpan.FromHours(4)) public static TimeSpan MaxSync = TimeSpan.FromHours(4);
public NexusPermissionsUpdater(ILogger<NexusKeyMaintainance> logger, AppSettings settings, QuickSync quickSync, DiscordWebHook discord, SqlService sql) : base(logger, settings, quickSync, TimeSpan.FromSeconds(1))
{ {
_discord = discord; _discord = discord;
_sql = sql; _sql = sql;
@ -24,7 +27,8 @@ namespace Wabbajack.Server.Services
public override async Task<int> Execute() public override async Task<int> Execute()
{ {
var permissions = await _sql.GetNexusPermissions(); await _sql.UpdateGameMetadata();
var data = await _sql.ModListArchives(); var data = await _sql.ModListArchives();
var nexusArchives = data.Select(a => a.State).OfType<NexusDownloader.State>().Select(d => (d.Game, d.ModID)) var nexusArchives = data.Select(a => a.State).OfType<NexusDownloader.State>().Select(d => (d.Game, d.ModID))
@ -33,38 +37,37 @@ namespace Wabbajack.Server.Services
_logger.LogInformation($"Starting nexus permissions updates for {nexusArchives.Count} mods"); _logger.LogInformation($"Starting nexus permissions updates for {nexusArchives.Count} mods");
using var queue = new WorkQueue(); using var queue = new WorkQueue(1);
var results = await nexusArchives.PMap(queue, async archive => var prev = await _sql.GetNexusPermissions();
{
var permissions = await HTMLInterface.GetUploadPermissions(archive.Game, archive.ModID);
return (archive.Game, archive.ModID, permissions);
});
var updated = 0; var lag = MaxSync / nexusArchives.Count * 2;
foreach (var result in results)
await nexusArchives.PMap(queue, async archive =>
{ {
if (permissions.TryGetValue((result.Game, result.ModID), out var oldPermission)) _logger.LogInformation($"Checking permissions for {archive.Game} {archive.ModID}");
var result = await HTMLInterface.GetUploadPermissions(archive.Game, archive.ModID);
await _sql.SetNexusPermission(archive.Game, archive.ModID, result);
if (prev.TryGetValue((archive.Game, archive.ModID), out var oldPermission))
{ {
if (oldPermission != result.permissions) if (oldPermission != result)
{ {
await _discord.Send(Channel.Spam, await _discord.Send(Channel.Spam,
new DiscordMessage { new DiscordMessage {
Content = $"Permissions status of {result.Game} {result.ModID} was {oldPermission} is now {result.permissions} " Content = $"Permissions status of {archive.Game} {archive.ModID} was {oldPermission} is now {result}"
}); });
await _sql.PurgeNexusCache(result.ModID); await _sql.PurgeNexusCache(archive.ModID);
updated += 1;
}
}
}
await _sql.SetNexusPermissions(results);
if (updated > 0)
await _quickSync.Notify<ListValidator>(); await _quickSync.Notify<ListValidator>();
}
}
await Task.Delay(lag);
});
return 1;
}
return updated;
}
} }
} }

View File

@ -125,7 +125,7 @@ namespace Wabbajack.Server.Services
private static string PatchName(Hash oldHash, Hash newHash) private static string PatchName(Hash oldHash, Hash newHash)
{ {
return $"{Consts.ArchiveUpdatesCDNFolder}\\{oldHash.ToHex()}_{newHash.ToHex()}"; return $"\\{oldHash.ToHex()}_{newHash.ToHex()}";
} }
private async Task CleanupOldPatches() private async Task CleanupOldPatches()
@ -155,7 +155,7 @@ namespace Wabbajack.Server.Services
if (pendingPatch != default) break; if (pendingPatch != default) break;
} }
var files = await client.GetListingAsync($"{Consts.ArchiveUpdatesCDNFolder}\\"); var files = await client.GetListingAsync($"\\");
_logger.LogInformation($"Found {files.Length} on the CDN"); _logger.LogInformation($"Found {files.Length} on the CDN");
var sqlFiles = await _sql.AllPatchHashes(); var sqlFiles = await _sql.AllPatchHashes();
@ -206,9 +206,6 @@ namespace Wabbajack.Server.Services
$"Uploading {patchFile.Size.ToFileSizeString()} patch file to CDN"); $"Uploading {patchFile.Size.ToFileSizeString()} patch file to CDN");
using var client = await GetBunnyCdnFtpClient(); using var client = await GetBunnyCdnFtpClient();
if (!await client.DirectoryExistsAsync(Consts.ArchiveUpdatesCDNFolder))
await client.CreateDirectoryAsync(Consts.ArchiveUpdatesCDNFolder);
await client.UploadFileAsync((string)patchFile, patchName, FtpRemoteExists.Overwrite); await client.UploadFileAsync((string)patchFile, patchName, FtpRemoteExists.Overwrite);
return; return;
} }
@ -230,7 +227,7 @@ namespace Wabbajack.Server.Services
private async Task<FtpClient> GetBunnyCdnFtpClient() private async Task<FtpClient> GetBunnyCdnFtpClient()
{ {
var info = await Utils.FromEncryptedJson<BunnyCdnFtpInfo>("bunny-cdn-ftp-info"); var info = await BunnyCdnFtpInfo.GetCreds(StorageSpace.Patches);
var client = new FtpClient(info.Hostname) {Credentials = new NetworkCredential(info.Username, info.Password)}; var client = new FtpClient(info.Hostname) {Credentials = new NetworkCredential(info.Username, info.Password)};
await client.ConnectAsync(); await client.ConnectAsync();
return client; return client;

View File

@ -69,6 +69,7 @@ namespace Wabbajack.Server
services.AddSingleton<PatchBuilder>(); services.AddSingleton<PatchBuilder>();
services.AddSingleton<CDNMirrorList>(); services.AddSingleton<CDNMirrorList>();
services.AddSingleton<NexusPermissionsUpdater>(); services.AddSingleton<NexusPermissionsUpdater>();
services.AddSingleton<MirrorUploader>();
services.AddMvc(); services.AddMvc();
services.AddControllers() services.AddControllers()
@ -125,6 +126,7 @@ namespace Wabbajack.Server
app.UseService<PatchBuilder>(); app.UseService<PatchBuilder>();
app.UseService<CDNMirrorList>(); app.UseService<CDNMirrorList>();
app.UseService<NexusPermissionsUpdater>(); app.UseService<NexusPermissionsUpdater>();
app.UseService<MirrorUploader>();
app.Use(next => app.Use(next =>
{ {