mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #1322 from wabbajack-tools/latest-server-fixes
latest server fixes
This commit is contained in:
commit
1dda6f2bec
@ -38,7 +38,8 @@ namespace Wabbajack.CLI
|
|||||||
typeof(PurgeArchive),
|
typeof(PurgeArchive),
|
||||||
typeof(AllKnownDownloadStates),
|
typeof(AllKnownDownloadStates),
|
||||||
typeof(VerifyAllDownloads),
|
typeof(VerifyAllDownloads),
|
||||||
typeof(HashBenchmark)
|
typeof(HashBenchmark),
|
||||||
|
typeof(StressTestURL)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
52
Wabbajack.CLI/Verbs/StressTestURL.cs
Normal file
52
Wabbajack.CLI/Verbs/StressTestURL.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommandLine;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Lib;
|
||||||
|
using Wabbajack.Lib.Downloaders;
|
||||||
|
|
||||||
|
namespace Wabbajack.CLI.Verbs
|
||||||
|
{
|
||||||
|
[Verb("stress-test-url", HelpText = "Verify a file rapidly, trying to make it fail")]
|
||||||
|
public class StressTestURL : AVerb
|
||||||
|
{
|
||||||
|
[Option('i', "input", Required = true, HelpText = "Input url to stress")]
|
||||||
|
public string Input { get; set; } = "";
|
||||||
|
private Uri _Input => new Uri(Input);
|
||||||
|
|
||||||
|
protected override async Task<ExitCode> Run()
|
||||||
|
{
|
||||||
|
using var queue = new WorkQueue();
|
||||||
|
var state = await DownloadDispatcher.Infer(_Input);
|
||||||
|
if (state == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Could not parse URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Performing initial download");
|
||||||
|
await using var temp = new TempFile();
|
||||||
|
var archive = new Archive(state!);
|
||||||
|
if (!await state!.Download(archive, temp.Path))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed initial download");
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = await temp.Path.FileHashAsync();
|
||||||
|
archive.Hash = hash!.Value;
|
||||||
|
archive.Size = temp.Path.Size;
|
||||||
|
Console.WriteLine($"Hash: {archive.Hash} Size: {archive.Size.ToFileSizeString()}");
|
||||||
|
|
||||||
|
await Enumerable.Range(0, 100000)
|
||||||
|
.PMap(queue, async idx =>
|
||||||
|
{
|
||||||
|
if (!await state.Verify(archive))
|
||||||
|
{
|
||||||
|
throw new Exception($"{idx} Verification failed");
|
||||||
|
}
|
||||||
|
Console.WriteLine($"{idx} Verification passed");
|
||||||
|
});
|
||||||
|
return ExitCode.Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -400,7 +400,7 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
public async Task<string> GetStringAsync(Uri uri, CancellationToken? token = null)
|
public async Task<string> GetStringAsync(Uri uri, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
if (!Downloader.IsCloudFlareProtected)
|
if (!Downloader.IsCloudFlareProtected)
|
||||||
return await Downloader.AuthedClient.GetStringAsync(uri);
|
return await Downloader.AuthedClient.GetStringAsync(uri, token);
|
||||||
|
|
||||||
|
|
||||||
using var driver = await Downloader.GetAuthedDriver();
|
using var driver = await Downloader.GetAuthedDriver();
|
||||||
@ -448,7 +448,7 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
return await Downloader.AuthedClient.GetAsync(uri);
|
return await Downloader.AuthedClient.GetAsync(uri);
|
||||||
|
|
||||||
using var driver = await Downloader.GetAuthedDriver();
|
using var driver = await Downloader.GetAuthedDriver();
|
||||||
TaskCompletionSource<Uri?> promise = new TaskCompletionSource<Uri?>();
|
TaskCompletionSource<Uri?> promise = new();
|
||||||
driver.DownloadHandler = uri1 =>
|
driver.DownloadHandler = uri1 =>
|
||||||
{
|
{
|
||||||
promise.SetResult(uri);
|
promise.SetResult(uri);
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
using Wabbajack.Common.Exceptions;
|
using Wabbajack.Common.Exceptions;
|
||||||
using Wabbajack.Common.Serialization.Json;
|
using Wabbajack.Common.Serialization.Json;
|
||||||
|
using Wabbajack.Lib.Http;
|
||||||
using Wabbajack.Lib.Validation;
|
using Wabbajack.Lib.Validation;
|
||||||
|
|
||||||
namespace Wabbajack.Lib.Downloaders
|
namespace Wabbajack.Lib.Downloaders
|
||||||
@ -54,19 +57,27 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
public override async Task<bool> Download(Archive a, AbsolutePath destination)
|
public override async Task<bool> Download(Archive a, AbsolutePath destination)
|
||||||
{
|
{
|
||||||
var state = await ToHttpState();
|
var state = await ToHttpState();
|
||||||
|
if (state == null)
|
||||||
|
return false;
|
||||||
return await state.Download(a, destination);
|
return await state.Download(a, destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HTTPDownloader.State> ToHttpState()
|
private async Task<HTTPDownloader.State?> ToHttpState()
|
||||||
{
|
{
|
||||||
var initialURL = $"https://drive.google.com/uc?id={Id}&export=download";
|
var initialURL = $"https://drive.google.com/uc?id={Id}&export=download";
|
||||||
var client = new Wabbajack.Lib.Http.Client();
|
var client = new Wabbajack.Lib.Http.Client();
|
||||||
using var response = await client.GetAsync(initialURL);
|
using var response = await client.GetAsync(initialURL);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
throw new HttpException((int)response.StatusCode, response.ReasonPhrase ?? "Unknown");
|
throw new HttpException((int)response.StatusCode, response.ReasonPhrase ?? "Unknown");
|
||||||
var regex = new Regex("(?<=/uc\\?export=download&confirm=).*(?=;id=)");
|
var cookies = response.GetSetCookies();
|
||||||
var confirm = regex.Match(await response.Content.ReadAsStringAsync());
|
var warning = cookies.FirstOrDefault(c => c.Key.StartsWith("download_warning_"));
|
||||||
var url = $"https://drive.google.com/uc?export=download&confirm={confirm}&id={Id}";
|
response.Dispose();
|
||||||
|
if (warning == default)
|
||||||
|
{
|
||||||
|
return new HTTPDownloader.State(initialURL) { Client = client };
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = $"https://drive.google.com/uc?export=download&confirm={warning.Value}&id={Id}";
|
||||||
var httpState = new HTTPDownloader.State(url) { Client = client };
|
var httpState = new HTTPDownloader.State(url) { Client = client };
|
||||||
return httpState;
|
return httpState;
|
||||||
}
|
}
|
||||||
@ -74,6 +85,8 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
public override async Task<bool> Verify(Archive a, CancellationToken? token)
|
public override async Task<bool> Verify(Archive a, CancellationToken? token)
|
||||||
{
|
{
|
||||||
var state = await ToHttpState();
|
var state = await ToHttpState();
|
||||||
|
if (state == null)
|
||||||
|
return false;
|
||||||
return await state.Verify(a, token);
|
return await state.Verify(a, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,111 +82,119 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
destination.Parent.CreateDirectory();
|
destination.Parent.CreateDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var fs = download ? await destination.Create() : null)
|
await using var fs = download ? await destination.Create() : null;
|
||||||
|
var client = Client ?? await ClientAPI.GetClient();
|
||||||
|
client.Headers.Add(("User-Agent", Consts.UserAgent));
|
||||||
|
|
||||||
|
foreach (var header in Headers)
|
||||||
{
|
{
|
||||||
var client = Client ?? await ClientAPI.GetClient();
|
var idx = header.IndexOf(':');
|
||||||
client.Headers.Add(("User-Agent", Consts.UserAgent));
|
var k = header.Substring(0, idx);
|
||||||
|
var v = header.Substring(idx + 1);
|
||||||
|
client.Headers.Add((k, v));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var header in Headers)
|
long totalRead = 0;
|
||||||
|
const int bufferSize = 1024 * 32 * 8;
|
||||||
|
|
||||||
|
Utils.Status($"Starting Download {a.Name ?? Url}", Percent.Zero);
|
||||||
|
var response = await client.GetAsync(Url, errorsAsExceptions:false, retry:false, token:token);
|
||||||
|
TOP:
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
response.Dispose();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream stream;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Utils.Error(ex, $"While downloading {Url}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var headerVar = a.Size == 0 ? "1" : a.Size.ToString();
|
||||||
|
long headerContentSize = 0;
|
||||||
|
if (response.Content.Headers.Contains("Content-Length"))
|
||||||
|
{
|
||||||
|
headerVar = response.Content.Headers.GetValues("Content-Length").FirstOrDefault();
|
||||||
|
if (headerVar != null)
|
||||||
|
long.TryParse(headerVar, out headerContentSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!download)
|
||||||
|
{
|
||||||
|
await stream.DisposeAsync();
|
||||||
|
response.Dispose();
|
||||||
|
if (a.Size != 0 && headerContentSize != 0)
|
||||||
|
return a.Size == headerContentSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsResume = response.Headers.AcceptRanges.FirstOrDefault(f => f == "bytes") != null;
|
||||||
|
|
||||||
|
var contentSize = headerVar != null ? long.Parse(headerVar) : 1;
|
||||||
|
|
||||||
|
await using (var webs = stream)
|
||||||
|
{
|
||||||
|
var buffer = new byte[bufferSize];
|
||||||
|
int readThisCycle = 0;
|
||||||
|
|
||||||
|
while (!(token ?? CancellationToken.None).IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var idx = header.IndexOf(':');
|
int read = 0;
|
||||||
var k = header.Substring(0, idx);
|
try
|
||||||
var v = header.Substring(idx + 1);
|
|
||||||
client.Headers.Add((k, v));
|
|
||||||
}
|
|
||||||
|
|
||||||
long totalRead = 0;
|
|
||||||
var bufferSize = 1024 * 32 * 8;
|
|
||||||
|
|
||||||
Utils.Status($"Starting Download {a.Name ?? Url}", Percent.Zero);
|
|
||||||
var response = await client.GetAsync(Url, errorsAsExceptions:false, retry:false, token:token);
|
|
||||||
TOP:
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream stream;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
stream = await response.Content.ReadAsStreamAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Utils.Error(ex, $"While downloading {Url}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var headerVar = a.Size == 0 ? "1" : a.Size.ToString();
|
|
||||||
long header_content_size = 0;
|
|
||||||
if (response.Content.Headers.Contains("Content-Length"))
|
|
||||||
{
|
|
||||||
headerVar = response.Content.Headers.GetValues("Content-Length").FirstOrDefault();
|
|
||||||
if (headerVar != null)
|
|
||||||
long.TryParse(headerVar, out header_content_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!download)
|
|
||||||
{
|
|
||||||
if (a.Size != 0 && header_content_size != 0)
|
|
||||||
return a.Size == header_content_size;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportsResume = response.Headers.AcceptRanges.FirstOrDefault(f => f == "bytes") != null;
|
|
||||||
|
|
||||||
var contentSize = headerVar != null ? long.Parse(headerVar) : 1;
|
|
||||||
|
|
||||||
using (var webs = stream)
|
|
||||||
{
|
|
||||||
var buffer = new byte[bufferSize];
|
|
||||||
int readThisCycle = 0;
|
|
||||||
|
|
||||||
while (!(token ?? CancellationToken.None).IsCancellationRequested)
|
|
||||||
{
|
{
|
||||||
int read = 0;
|
read = await webs.ReadAsync(buffer, 0, bufferSize);
|
||||||
try
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
if (readThisCycle == 0)
|
||||||
{
|
{
|
||||||
read = await webs.ReadAsync(buffer, 0, bufferSize);
|
await stream.DisposeAsync();
|
||||||
|
response.Dispose();
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
|
||||||
|
if (totalRead < contentSize)
|
||||||
{
|
{
|
||||||
if (readThisCycle == 0)
|
if (!supportsResume)
|
||||||
throw;
|
|
||||||
|
|
||||||
if (totalRead < contentSize)
|
|
||||||
{
|
{
|
||||||
if (supportsResume)
|
await stream.DisposeAsync();
|
||||||
{
|
response.Dispose();
|
||||||
Utils.Log(
|
|
||||||
$"Abort during download, trying to resume {Url} from {totalRead.ToFileSizeString()}");
|
|
||||||
|
|
||||||
var msg = new HttpRequestMessage(HttpMethod.Get, Url);
|
|
||||||
msg.Headers.Range = new RangeHeaderValue(totalRead, null);
|
|
||||||
response.Dispose();
|
|
||||||
response = await client.SendAsync(msg);
|
|
||||||
goto TOP;
|
|
||||||
}
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
Utils.Log(
|
||||||
|
$"Abort during download, trying to resume {Url} from {totalRead.ToFileSizeString()}");
|
||||||
|
|
||||||
|
var msg = new HttpRequestMessage(HttpMethod.Get, Url);
|
||||||
|
msg.Headers.Range = new RangeHeaderValue(totalRead, null);
|
||||||
|
response.Dispose();
|
||||||
|
response = await client.SendAsync(msg);
|
||||||
|
goto TOP;
|
||||||
}
|
}
|
||||||
|
|
||||||
readThisCycle += read;
|
break;
|
||||||
|
|
||||||
if (read == 0) break;
|
|
||||||
Utils.Status($"Downloading {a.Name}", Percent.FactoryPutInRange(totalRead, contentSize));
|
|
||||||
|
|
||||||
fs!.Write(buffer, 0, read);
|
|
||||||
totalRead += read;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readThisCycle += read;
|
||||||
|
|
||||||
|
if (read == 0) break;
|
||||||
|
Utils.Status($"Downloading {a.Name}", Percent.FactoryPutInRange(totalRead, contentSize));
|
||||||
|
|
||||||
|
fs!.Write(buffer, 0, read);
|
||||||
|
totalRead += read;
|
||||||
}
|
}
|
||||||
response.Dispose();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
response.Dispose();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<bool> Verify(Archive a, CancellationToken? token)
|
public override async Task<bool> Verify(Archive a, CancellationToken? token)
|
||||||
|
@ -48,10 +48,10 @@ namespace Wabbajack.Lib.Http
|
|||||||
return await SendStringAsync(request, token: token);
|
return await SendStringAsync(request, token: token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetStringAsync(Uri url)
|
public async Task<string> GetStringAsync(Uri url, CancellationToken? token = null)
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
return await SendStringAsync(request);
|
return await SendStringAsync(request, token: token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> DeleteStringAsync(string url)
|
public async Task<string> DeleteStringAsync(string url)
|
||||||
|
@ -22,6 +22,7 @@ namespace Wabbajack.Lib.Http
|
|||||||
MaxConnectionsPerServer = 20,
|
MaxConnectionsPerServer = 20,
|
||||||
PooledConnectionLifetime = TimeSpan.FromMilliseconds(100),
|
PooledConnectionLifetime = TimeSpan.FromMilliseconds(100),
|
||||||
PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(100),
|
PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(100),
|
||||||
|
AutomaticDecompression = DecompressionMethods.All
|
||||||
|
|
||||||
};
|
};
|
||||||
Utils.Log($"Configuring with SSL {_socketsHandler.SslOptions.EnabledSslProtocols}");
|
Utils.Log($"Configuring with SSL {_socketsHandler.SslOptions.EnabledSslProtocols}");
|
||||||
|
22
Wabbajack.Lib/Http/HttpExtensions.cs
Normal file
22
Wabbajack.Lib/Http/HttpExtensions.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace Wabbajack.Lib.Http
|
||||||
|
{
|
||||||
|
public static class HttpExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<(string Key, string Value)> GetSetCookies(this HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
if (!response.Headers.TryGetValues("set-cookie", out var values))
|
||||||
|
return Array.Empty<(string, string)>();
|
||||||
|
|
||||||
|
return values
|
||||||
|
.SelectMany(h => h.Split(";"))
|
||||||
|
.Select(h => h.Split("="))
|
||||||
|
.Where(h => h.Length == 2)
|
||||||
|
.Select(h => (h[0], h[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -69,7 +69,7 @@ namespace Wabbajack.Lib
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="action"></param>
|
/// <param name="action"></param>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
public static async Task Send(string action, string value)
|
public static async Task Send(string action, string subject)
|
||||||
{
|
{
|
||||||
if (BuildServerStatus.IsBuildServerDown)
|
if (BuildServerStatus.IsBuildServerDown)
|
||||||
return;
|
return;
|
||||||
@ -78,7 +78,7 @@ namespace Wabbajack.Lib
|
|||||||
Utils.Log($"File hash check (-42) {key}");
|
Utils.Log($"File hash check (-42) {key}");
|
||||||
var client = new Http.Client();
|
var client = new Http.Client();
|
||||||
client.Headers.Add((Consts.MetricsKeyHeader, key));
|
client.Headers.Add((Consts.MetricsKeyHeader, key));
|
||||||
await client.GetAsync($"{Consts.WabbajackBuildServerUri}metrics/{action}/{value}");
|
await client.GetAsync($"{Consts.WabbajackBuildServerUri}metrics/{action}/{subject}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Error(Type type, Exception exception)
|
public static async Task Error(Type type, Exception exception)
|
||||||
|
@ -15,6 +15,7 @@ using Wabbajack.Lib.ModListRegistry;
|
|||||||
using Wabbajack.Server;
|
using Wabbajack.Server;
|
||||||
using Wabbajack.Server.DataLayer;
|
using Wabbajack.Server.DataLayer;
|
||||||
using Wabbajack.Server.DTOs;
|
using Wabbajack.Server.DTOs;
|
||||||
|
using Wabbajack.Server.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
@ -166,6 +167,8 @@ namespace Wabbajack.BuildServer.Test
|
|||||||
_client = new Wabbajack.Lib.Http.Client();
|
_client = new Wabbajack.Lib.Http.Client();
|
||||||
_authedClient = new Wabbajack.Lib.Http.Client();
|
_authedClient = new Wabbajack.Lib.Http.Client();
|
||||||
Fixture = fixture.Deref();
|
Fixture = fixture.Deref();
|
||||||
|
var cache = Fixture.GetService<MetricsKeyCache>();
|
||||||
|
cache.AddKey(Metrics.GetMetricsKey().Result);
|
||||||
_authedClient.Headers.Add(("x-api-key", Fixture.APIKey));
|
_authedClient.Headers.Add(("x-api-key", Fixture.APIKey));
|
||||||
AuthorAPI.ApiKeyOverride = Fixture.APIKey;
|
AuthorAPI.ApiKeyOverride = Fixture.APIKey;
|
||||||
_queue = new WorkQueue();
|
_queue = new WorkQueue();
|
||||||
|
@ -5,6 +5,7 @@ using Dapper;
|
|||||||
using Wabbajack.Lib;
|
using Wabbajack.Lib;
|
||||||
using Wabbajack.Server.DataLayer;
|
using Wabbajack.Server.DataLayer;
|
||||||
using Wabbajack.Server.DTOs;
|
using Wabbajack.Server.DTOs;
|
||||||
|
using Wabbajack.Server.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
@ -34,7 +35,20 @@ namespace Wabbajack.BuildServer.Test
|
|||||||
using var response = await _client.GetAsync(MakeURL($"metrics/report/{action}"));
|
using var response = await _client.GetAsync(MakeURL($"metrics/report/{action}"));
|
||||||
Assert.Equal(TimeSpan.FromHours(1), response.Headers.CacheControl.MaxAge);
|
Assert.Equal(TimeSpan.FromHours(1), response.Headers.CacheControl.MaxAge);
|
||||||
// we'll just make sure this doesn't error, with limited data that's about all we can do atm
|
// we'll just make sure this doesn't error, with limited data that's about all we can do atm
|
||||||
|
|
||||||
|
using var totalInstalls = await _client.GetAsync(MakeURL($"metrics/total_installs.html"));
|
||||||
|
Assert.True(totalInstalls.IsSuccessStatusCode);
|
||||||
|
|
||||||
|
using var totalUniqueInstalls = await _client.GetAsync(MakeURL($"metrics/total_unique_installs.html"));
|
||||||
|
Assert.True(totalUniqueInstalls.IsSuccessStatusCode);
|
||||||
|
|
||||||
|
using var dumpResponse = await _client.GetAsync(MakeURL("metrics/dump.json"));
|
||||||
|
Assert.True(dumpResponse.IsSuccessStatusCode);
|
||||||
|
var data = await dumpResponse.Content.ReadAsStringAsync();
|
||||||
|
Assert.NotEmpty(data);
|
||||||
|
|
||||||
|
var cache = Fixture.GetService<MetricsKeyCache>();
|
||||||
|
Assert.True(await cache.KeyCount() > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -550,6 +550,25 @@ CREATE TABLE [dbo].[ModListArchives](
|
|||||||
)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]
|
)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]
|
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
|
||||||
GO
|
GO
|
||||||
|
|
||||||
|
/****** Object: Table [dbo].[MetricsKeys] Script Date: 2/17/2021 9:20:27 PM ******/
|
||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE TABLE [dbo].[MetricsKeys](
|
||||||
|
[MetricsKey] [varchar](64) NOT NULL,
|
||||||
|
CONSTRAINT [PK_MetricsKeys] PRIMARY KEY CLUSTERED
|
||||||
|
(
|
||||||
|
[MetricsKey] 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]
|
||||||
|
GO
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/****** Object: Table [dbo].[ModListArchiveStatus] Script Date: 12/29/2020 8:55:04 PM ******/
|
/****** Object: Table [dbo].[ModListArchiveStatus] Script Date: 12/29/2020 8:55:04 PM ******/
|
||||||
SET ANSI_NULLS ON
|
SET ANSI_NULLS ON
|
||||||
GO
|
GO
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
@ -13,6 +14,7 @@ using Wabbajack.Common;
|
|||||||
using Wabbajack.Common.Serialization.Json;
|
using Wabbajack.Common.Serialization.Json;
|
||||||
using Wabbajack.Server.DataLayer;
|
using Wabbajack.Server.DataLayer;
|
||||||
using Wabbajack.Server.DTOs;
|
using Wabbajack.Server.DTOs;
|
||||||
|
using Wabbajack.Server.Services;
|
||||||
|
|
||||||
|
|
||||||
namespace Wabbajack.BuildServer
|
namespace Wabbajack.BuildServer
|
||||||
@ -29,17 +31,20 @@ namespace Wabbajack.BuildServer
|
|||||||
{
|
{
|
||||||
private const string ProblemDetailsContentType = "application/problem+json";
|
private const string ProblemDetailsContentType = "application/problem+json";
|
||||||
private readonly SqlService _sql;
|
private readonly SqlService _sql;
|
||||||
private static ConcurrentHashSet<string> _knownKeys = new();
|
|
||||||
private const string ApiKeyHeaderName = "X-Api-Key";
|
private const string ApiKeyHeaderName = "X-Api-Key";
|
||||||
|
|
||||||
|
private MetricsKeyCache _keyCache;
|
||||||
|
|
||||||
public ApiKeyAuthenticationHandler(
|
public ApiKeyAuthenticationHandler(
|
||||||
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
|
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
ISystemClock clock,
|
ISystemClock clock,
|
||||||
|
MetricsKeyCache keyCache,
|
||||||
SqlService db) : base(options, logger, encoder, clock)
|
SqlService db) : base(options, logger, encoder, clock)
|
||||||
{
|
{
|
||||||
_sql = db;
|
_sql = db;
|
||||||
|
_keyCache = keyCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
@ -51,19 +56,25 @@ namespace Wabbajack.BuildServer
|
|||||||
{
|
{
|
||||||
if (await _sql.IsTarKey(metricsKey))
|
if (await _sql.IsTarKey(metricsKey))
|
||||||
{
|
{
|
||||||
await _sql.IngestMetric(new Metric {Action = "TarKey", Subject = "Auth", MetricsKey = metricsKey, Timestamp = DateTime.UtcNow});
|
await _sql.IngestMetric(new Metric
|
||||||
|
{
|
||||||
|
Action = "TarKey",
|
||||||
|
Subject = "Auth",
|
||||||
|
MetricsKey = metricsKey,
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
await Task.Delay(TimeSpan.FromSeconds(60));
|
await Task.Delay(TimeSpan.FromSeconds(60));
|
||||||
throw new Exception("Error, lipsum timeout of the cross distant cloud.");
|
throw new Exception("Error, lipsum timeout of the cross distant cloud.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var authorKey = Request.Headers[ApiKeyHeaderName].FirstOrDefault();
|
var authorKey = Request.Headers[ApiKeyHeaderName].FirstOrDefault();
|
||||||
|
|
||||||
if (authorKey == null && metricsKey == null)
|
if (authorKey == null && metricsKey == null)
|
||||||
{
|
{
|
||||||
return AuthenticateResult.NoResult();
|
return AuthenticateResult.NoResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (authorKey != null)
|
if (authorKey != null)
|
||||||
{
|
{
|
||||||
@ -83,15 +94,14 @@ namespace Wabbajack.BuildServer
|
|||||||
|
|
||||||
return AuthenticateResult.Success(ticket);
|
return AuthenticateResult.Success(ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!_knownKeys.Contains(metricsKey) && !await _sql.ValidMetricsKey(metricsKey))
|
if (!await _keyCache.IsValidKey(metricsKey))
|
||||||
{
|
{
|
||||||
return AuthenticateResult.Fail("Invalid Metrics Key");
|
return AuthenticateResult.Fail("Invalid Metrics Key");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_knownKeys.Add(metricsKey);
|
|
||||||
|
|
||||||
var claims = new List<Claim> {new(ClaimTypes.Role, "User")};
|
var claims = new List<Claim> {new(ClaimTypes.Role, "User")};
|
||||||
|
|
||||||
|
|
||||||
@ -102,7 +112,6 @@ namespace Wabbajack.BuildServer
|
|||||||
|
|
||||||
return AuthenticateResult.Success(ticket);
|
return AuthenticateResult.Success(ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonName("RequestLog")]
|
[JsonName("RequestLog")]
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Nettle;
|
using Nettle;
|
||||||
|
using Wabbajack.Common;
|
||||||
using Wabbajack.Common.StatusFeed;
|
using Wabbajack.Common.StatusFeed;
|
||||||
using Wabbajack.Server;
|
using Wabbajack.Server;
|
||||||
using Wabbajack.Server.DataLayer;
|
using Wabbajack.Server.DataLayer;
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Nettle;
|
using Nettle;
|
||||||
@ -11,6 +15,7 @@ using Wabbajack.Common;
|
|||||||
using Wabbajack.Server;
|
using Wabbajack.Server;
|
||||||
using Wabbajack.Server.DataLayer;
|
using Wabbajack.Server.DataLayer;
|
||||||
using Wabbajack.Server.DTOs;
|
using Wabbajack.Server.DTOs;
|
||||||
|
using Wabbajack.Server.Services;
|
||||||
using WebSocketSharp;
|
using WebSocketSharp;
|
||||||
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||||
|
|
||||||
@ -22,11 +27,13 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
{
|
{
|
||||||
private SqlService _sql;
|
private SqlService _sql;
|
||||||
private ILogger<MetricsController> _logger;
|
private ILogger<MetricsController> _logger;
|
||||||
|
private MetricsKeyCache _keyCache;
|
||||||
|
|
||||||
public MetricsController(ILogger<MetricsController> logger, SqlService sql)
|
public MetricsController(ILogger<MetricsController> logger, SqlService sql, MetricsKeyCache keyCache)
|
||||||
{
|
{
|
||||||
_sql = sql;
|
_sql = sql;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_keyCache = keyCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -34,7 +41,15 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
public async Task<Result> LogMetricAsync(string subject, string value)
|
public async Task<Result> LogMetricAsync(string subject, string value)
|
||||||
{
|
{
|
||||||
var date = DateTime.UtcNow;
|
var date = DateTime.UtcNow;
|
||||||
await Log(date, subject, value, Request.Headers[Consts.MetricsKeyHeader].FirstOrDefault());
|
var metricsKey = Request.Headers[Consts.MetricsKeyHeader].FirstOrDefault();
|
||||||
|
if (metricsKey != null)
|
||||||
|
await _keyCache.AddKey(metricsKey);
|
||||||
|
|
||||||
|
// Used in tests
|
||||||
|
if (value == "Default" || value == "untitled" || subject == "failed_download" || Guid.TryParse(value, out _))
|
||||||
|
return new Result { Timestamp = date};
|
||||||
|
|
||||||
|
await Log(date, subject, value, metricsKey);
|
||||||
return new Result { Timestamp = date};
|
return new Result { Timestamp = date};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +89,7 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
|
|
||||||
return Ok(results == 0
|
return Ok(results == 0
|
||||||
? new Badge($"Modlist {name} not found!", "Error") {color = "red"}
|
? new Badge($"Modlist {name} not found!", "Error") {color = "red"}
|
||||||
: new Badge("Installations: ", $"{results}") {color = "green"});
|
: new Badge("Installations: ", "____") {color = "green"});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -87,7 +102,7 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
|
|
||||||
return Ok(results == 0
|
return Ok(results == 0
|
||||||
? new Badge($"Modlist {name} not found!", "Error") {color = "red"}
|
? new Badge($"Modlist {name} not found!", "Error") {color = "red"}
|
||||||
: new Badge("Installations: ", $"{results}"){color = "green"}) ;
|
: new Badge("Installations: ", "____"){color = "green"}) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Func<object, string> ReportTemplate = NettleEngine.GetCompiler().Compile(@"
|
private static readonly Func<object, string> ReportTemplate = NettleEngine.GetCompiler().Compile(@"
|
||||||
@ -149,5 +164,82 @@ namespace Wabbajack.BuildServer.Controllers
|
|||||||
{
|
{
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TotalListTemplateData
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public long Total { get; set; }
|
||||||
|
public Item[] Items { get; set; }
|
||||||
|
|
||||||
|
public class Item
|
||||||
|
{
|
||||||
|
public long Count { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<object, string> _totalListTemplate;
|
||||||
|
|
||||||
|
|
||||||
|
private static Func<object, string> TotalListTemplate
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_totalListTemplate == null)
|
||||||
|
{
|
||||||
|
var resource = Assembly.GetExecutingAssembly()
|
||||||
|
.GetManifestResourceStream("Wabbajack.Server.Controllers.Templates.TotalListTemplate.html")!
|
||||||
|
.ReadAll();
|
||||||
|
_totalListTemplate = NettleEngine.GetCompiler().Compile(Encoding.UTF8.GetString(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _totalListTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("total_installs.html")]
|
||||||
|
[ResponseCache(Duration = 60 * 60)]
|
||||||
|
public async Task<ContentResult> TotalInstalls()
|
||||||
|
{
|
||||||
|
var data = await _sql.GetTotalInstalls();
|
||||||
|
var result = TotalListTemplate(new TotalListTemplateData
|
||||||
|
{
|
||||||
|
Title = "Total Installs",
|
||||||
|
Total = data.Sum(d => d.Item2),
|
||||||
|
Items = data.Select(d => new TotalListTemplateData.Item {Title = d.Item1, Count = d.Item2})
|
||||||
|
.ToArray()
|
||||||
|
});
|
||||||
|
return new ContentResult {
|
||||||
|
ContentType = "text/html",
|
||||||
|
StatusCode = (int)HttpStatusCode.OK,
|
||||||
|
Content = result};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("total_unique_installs.html")]
|
||||||
|
[ResponseCache(Duration = 60 * 60)]
|
||||||
|
public async Task<ContentResult> TotalUniqueInstalls()
|
||||||
|
{
|
||||||
|
var data = await _sql.GetTotalUniqueInstalls();
|
||||||
|
var result = TotalListTemplate(new TotalListTemplateData
|
||||||
|
{
|
||||||
|
Title = "Total Unique Installs",
|
||||||
|
Total = data.Sum(d => d.Item2),
|
||||||
|
Items = data.Select(d => new TotalListTemplateData.Item {Title = d.Item1, Count = d.Item2})
|
||||||
|
.ToArray()
|
||||||
|
});
|
||||||
|
return new ContentResult {
|
||||||
|
ContentType = "text/html",
|
||||||
|
StatusCode = (int)HttpStatusCode.OK,
|
||||||
|
Content = result};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("dump.json")]
|
||||||
|
public async Task<IActionResult> DataDump()
|
||||||
|
{
|
||||||
|
return Ok(await _sql.MetricsDump().ToArrayAsync());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Total Installs</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2>{{$.Title}} - Total: {{$.Total}}</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
{{each $.Items }}
|
||||||
|
<tr>
|
||||||
|
<td>{{$.Count}}</td>
|
||||||
|
<td>{{$.Title}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -70,8 +70,28 @@ namespace Wabbajack.Server.DataLayer
|
|||||||
public async Task<bool> ValidMetricsKey(string metricsKey)
|
public async Task<bool> ValidMetricsKey(string metricsKey)
|
||||||
{
|
{
|
||||||
await using var conn = await Open();
|
await using var conn = await Open();
|
||||||
return (await conn.QueryAsync<string>("SELECT TOP(1) MetricsKey from Metrics Where MetricsKey = @MetricsKey",
|
return (await conn.QuerySingleOrDefaultAsync<string>("SELECT TOP(1) MetricsKey from dbo.MetricsKeys Where MetricsKey = @MetricsKey",
|
||||||
new {MetricsKey = metricsKey})).FirstOrDefault() != null;
|
new {MetricsKey = metricsKey})) != default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddMetricsKey(string metricsKey)
|
||||||
|
{
|
||||||
|
await using var conn = await Open();
|
||||||
|
await using var trans = conn.BeginTransaction();
|
||||||
|
|
||||||
|
if ((await conn.QuerySingleOrDefaultAsync<string>(
|
||||||
|
"SELECT TOP(1) MetricsKey from dbo.MetricsKeys Where MetricsKey = @MetricsKey",
|
||||||
|
new {MetricsKey = metricsKey}, trans)) != default)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await conn.ExecuteAsync("INSERT INTO dbo.MetricsKeys (MetricsKey) VALUES (@MetricsKey)",
|
||||||
|
new {MetricsKey = metricsKey}, trans);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string[]> AllKeys()
|
||||||
|
{
|
||||||
|
await using var conn = await Open();
|
||||||
|
return (await conn.QueryAsync<string>("SELECT MetricsKey from dbo.MetricsKeys")).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -95,5 +115,72 @@ namespace Wabbajack.Server.DataLayer
|
|||||||
WHERE JSON_VALUE(Metadata, '$.links.machineURL') = @MachineURL)",
|
WHERE JSON_VALUE(Metadata, '$.links.machineURL') = @MachineURL)",
|
||||||
new {MachineURL = machineUrl});
|
new {MachineURL = machineUrl});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<(string, long)>> GetTotalInstalls()
|
||||||
|
{
|
||||||
|
await using var conn = await Open();
|
||||||
|
return await conn.QueryAsync<(string, long)>(
|
||||||
|
@"SELECT GroupingSubject, Count(*) as Count
|
||||||
|
From dbo.Metrics
|
||||||
|
WHERE
|
||||||
|
|
||||||
|
GroupingSubject in (select DISTINCT GroupingSubject from dbo.Metrics
|
||||||
|
WHERE action = 'finish_install'
|
||||||
|
AND MetricsKey is not null)
|
||||||
|
group by GroupingSubject
|
||||||
|
order by Count(*) desc");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<(string, long)>> GetTotalUniqueInstalls()
|
||||||
|
{
|
||||||
|
await using var conn = await Open();
|
||||||
|
return await conn.QueryAsync<(string, long)>(
|
||||||
|
@"Select GroupingSubject, Count(*) as Count
|
||||||
|
FROM
|
||||||
|
(select DISTINCT MetricsKey, GroupingSubject
|
||||||
|
From dbo.Metrics
|
||||||
|
WHERE
|
||||||
|
GroupingSubject in (select DISTINCT GroupingSubject from dbo.Metrics
|
||||||
|
WHERE action = 'finish_install'
|
||||||
|
AND MetricsKey is not null)) m
|
||||||
|
GROUP BY GroupingSubject
|
||||||
|
Order by Count(*) desc
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<MetricRow> MetricsDump()
|
||||||
|
{
|
||||||
|
var keys = new Dictionary<string, long>();
|
||||||
|
|
||||||
|
await using var conn = await Open();
|
||||||
|
foreach (var row in await conn.QueryAsync<(long, DateTime, string, string, string, string)>(@"select Id, Timestamp, Action, Subject, MetricsKey, GroupingSubject from dbo.metrics WHERE MetricsKey is not null"))
|
||||||
|
{
|
||||||
|
if (!keys.TryGetValue(row.Item5, out var keyid))
|
||||||
|
{
|
||||||
|
keyid = keys.Count;
|
||||||
|
keys[row.Item5] = keyid;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return new MetricRow
|
||||||
|
{
|
||||||
|
Id = row.Item1,
|
||||||
|
Timestamp = row.Item2,
|
||||||
|
Action = row.Item3,
|
||||||
|
Subject = row.Item4,
|
||||||
|
MetricsKey = keyid,
|
||||||
|
GroupingSubject = row.Item6
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MetricRow
|
||||||
|
{
|
||||||
|
public long Id;
|
||||||
|
public DateTime Timestamp;
|
||||||
|
public string Action;
|
||||||
|
public string Subject;
|
||||||
|
public string GroupingSubject;
|
||||||
|
public long MetricsKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,9 @@ namespace Wabbajack.Server.Services
|
|||||||
private QuickSync _quickSync;
|
private QuickSync _quickSync;
|
||||||
private DiscordSocketClient _client;
|
private DiscordSocketClient _client;
|
||||||
private SqlService _sql;
|
private SqlService _sql;
|
||||||
|
private MetricsKeyCache _keyCache;
|
||||||
|
|
||||||
public DiscordFrontend(ILogger<DiscordFrontend> logger, AppSettings settings, QuickSync quickSync, SqlService sql)
|
public DiscordFrontend(ILogger<DiscordFrontend> logger, AppSettings settings, QuickSync quickSync, SqlService sql, MetricsKeyCache keyCache)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
@ -33,6 +34,7 @@ namespace Wabbajack.Server.Services
|
|||||||
_client.MessageReceived += MessageReceivedAsync;
|
_client.MessageReceived += MessageReceivedAsync;
|
||||||
|
|
||||||
_sql = sql;
|
_sql = sql;
|
||||||
|
_keyCache = keyCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task MessageReceivedAsync(SocketMessage arg)
|
private async Task MessageReceivedAsync(SocketMessage arg)
|
||||||
@ -88,6 +90,10 @@ namespace Wabbajack.Server.Services
|
|||||||
await ReplyTo(arg, $"Purged all traces of #{parts[2]} from the server, triggered list downloading. {deleted} records removed");
|
await ReplyTo(arg, $"Purged all traces of #{parts[2]} from the server, triggered list downloading. {deleted} records removed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (parts[1] == "users")
|
||||||
|
{
|
||||||
|
await ReplyTo(arg, $"Wabbajack has {await _keyCache.KeyCount()} known unique users");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +197,7 @@ namespace Wabbajack.Server.Services
|
|||||||
private AsyncLock _healLock = new AsyncLock();
|
private AsyncLock _healLock = new AsyncLock();
|
||||||
private async Task<(Archive, ArchiveStatus)> TryToHeal(ValidationData data, Archive archive, ModlistMetadata modList)
|
private async Task<(Archive, ArchiveStatus)> TryToHeal(ValidationData data, Archive archive, ModlistMetadata modList)
|
||||||
{
|
{
|
||||||
|
using var _ = await _healLock.WaitAsync();
|
||||||
var srcDownload = await _sql.GetArchiveDownload(archive.State.PrimaryKeyString, archive.Hash, archive.Size);
|
var srcDownload = await _sql.GetArchiveDownload(archive.State.PrimaryKeyString, archive.Hash, archive.Size);
|
||||||
if (srcDownload == null || srcDownload.IsFailed == true)
|
if (srcDownload == null || srcDownload.IsFailed == true)
|
||||||
{
|
{
|
||||||
@ -204,7 +205,7 @@ namespace Wabbajack.Server.Services
|
|||||||
return (archive, ArchiveStatus.InValid);
|
return (archive, ArchiveStatus.InValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var patches = await _sql.PatchesForSource(archive.Hash);
|
var patches = await _sql.PatchesForSource(archive.Hash);
|
||||||
foreach (var patch in patches)
|
foreach (var patch in patches)
|
||||||
{
|
{
|
||||||
@ -219,7 +220,7 @@ namespace Wabbajack.Server.Services
|
|||||||
return (archive, ArchiveStatus.Updated);
|
return (archive, ArchiveStatus.Updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var _ = await _healLock.WaitAsync();
|
|
||||||
var upgradeTime = DateTime.UtcNow;
|
var upgradeTime = DateTime.UtcNow;
|
||||||
_logger.LogInformation($"Validator Finding Upgrade for {archive.Hash} {archive.State.PrimaryKeyString}");
|
_logger.LogInformation($"Validator Finding Upgrade for {archive.Hash} {archive.State.PrimaryKeyString}");
|
||||||
|
|
||||||
|
62
Wabbajack.Server/Services/MetricsKeyCache.cs
Normal file
62
Wabbajack.Server/Services/MetricsKeyCache.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Server.DataLayer;
|
||||||
|
|
||||||
|
namespace Wabbajack.Server.Services
|
||||||
|
{
|
||||||
|
public class MetricsKeyCache : IStartable
|
||||||
|
{
|
||||||
|
private ILogger<MetricsKeyCache> _logger;
|
||||||
|
private SqlService _sql;
|
||||||
|
private HashSet<string> _knownKeys = new();
|
||||||
|
private AsyncLock _lock = new();
|
||||||
|
|
||||||
|
public MetricsKeyCache(ILogger<MetricsKeyCache> logger, SqlService sql)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_sql = sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsValidKey(string key)
|
||||||
|
{
|
||||||
|
using (var _ = await _lock.WaitAsync())
|
||||||
|
{
|
||||||
|
if (_knownKeys.Contains(key)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await _sql.ValidMetricsKey(key))
|
||||||
|
{
|
||||||
|
using var _ = await _lock.WaitAsync();
|
||||||
|
_knownKeys.Add(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddKey(string key)
|
||||||
|
{
|
||||||
|
using (var _ = await _lock.WaitAsync())
|
||||||
|
{
|
||||||
|
if (_knownKeys.Contains(key)) return;
|
||||||
|
_knownKeys.Add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _sql.AddMetricsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_knownKeys = (_sql.AllKeys().Result).ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> KeyCount()
|
||||||
|
{
|
||||||
|
using var _ = await _lock.WaitAsync();
|
||||||
|
return _knownKeys.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Wabbajack.BuildServer;
|
using Wabbajack.BuildServer;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Common.Exceptions;
|
||||||
using Wabbajack.Lib.NexusApi;
|
using Wabbajack.Lib.NexusApi;
|
||||||
using Wabbajack.Server.DataLayer;
|
using Wabbajack.Server.DataLayer;
|
||||||
|
|
||||||
@ -28,13 +29,12 @@ namespace Wabbajack.Server.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = new TrackingClient(_sql, key);
|
var client = new TrackingClient(_sql, key);
|
||||||
if (!await client.IsPremium())
|
if (await client.IsPremium())
|
||||||
{
|
return client;
|
||||||
_logger.LogWarning($"Purging non premium key");
|
|
||||||
await _sql.DeleteNexusAPIKey(key.Key);
|
_logger.LogWarning($"Purging non premium key");
|
||||||
continue;
|
await _sql.DeleteNexusAPIKey(key.Key);
|
||||||
}
|
continue;
|
||||||
return client;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -69,13 +69,16 @@ namespace Wabbajack.Server.Services
|
|||||||
var (daily, hourly) = await client.GetRemainingApiCalls();
|
var (daily, hourly) = await client.GetRemainingApiCalls();
|
||||||
await _sql.SetNexusAPIKey(key.Key, daily, hourly);
|
await _sql.SetNexusAPIKey(key.Key, daily, hourly);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (HttpException ex)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Warning, "Update error, purging API key");
|
_logger.Log(LogLevel.Warning, $"Nexus error, not purging API key : {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Log(LogLevel.Warning, $"Update error, purging API key : {ex.Message}");
|
||||||
await _sql.DeleteNexusAPIKey(key.Key);
|
await _sql.DeleteNexusAPIKey(key.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys.Count;
|
return keys.Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,8 +41,8 @@ namespace Wabbajack.Server.Services
|
|||||||
bool isValid = false;
|
bool isValid = false;
|
||||||
switch (archive.State)
|
switch (archive.State)
|
||||||
{
|
{
|
||||||
case WabbajackCDNDownloader.State _:
|
case WabbajackCDNDownloader.State _:
|
||||||
case GoogleDriveDownloader.State _:
|
//case GoogleDriveDownloader.State _: // Let's try validating Google again 2/10/2021
|
||||||
case ManualDownloader.State _:
|
case ManualDownloader.State _:
|
||||||
case ModDBDownloader.State _:
|
case ModDBDownloader.State _:
|
||||||
case HTTPDownloader.State h when h.Url.StartsWith("https://wabbajack"):
|
case HTTPDownloader.State h when h.Url.StartsWith("https://wabbajack"):
|
||||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -76,6 +77,13 @@ namespace Wabbajack.Server
|
|||||||
services.AddSingleton<Watchdog>();
|
services.AddSingleton<Watchdog>();
|
||||||
services.AddSingleton<DiscordFrontend>();
|
services.AddSingleton<DiscordFrontend>();
|
||||||
services.AddSingleton<AuthoredFilesCleanup>();
|
services.AddSingleton<AuthoredFilesCleanup>();
|
||||||
|
services.AddSingleton<MetricsKeyCache>();
|
||||||
|
services.AddResponseCompression(options =>
|
||||||
|
{
|
||||||
|
options.Providers.Add<BrotliCompressionProvider>();
|
||||||
|
options.Providers.Add<GzipCompressionProvider>();
|
||||||
|
options.MimeTypes = new[] {"application/json"};
|
||||||
|
});
|
||||||
|
|
||||||
services.AddMvc();
|
services.AddMvc();
|
||||||
services.AddControllers()
|
services.AddControllers()
|
||||||
@ -123,6 +131,7 @@ namespace Wabbajack.Server
|
|||||||
app.UseNexusPoll();
|
app.UseNexusPoll();
|
||||||
app.UseArchiveMaintainer();
|
app.UseArchiveMaintainer();
|
||||||
app.UseModListDownloader();
|
app.UseModListDownloader();
|
||||||
|
app.UseResponseCompression();
|
||||||
|
|
||||||
app.UseService<NonNexusDownloadValidator>();
|
app.UseService<NonNexusDownloadValidator>();
|
||||||
app.UseService<ListValidator>();
|
app.UseService<ListValidator>();
|
||||||
@ -137,6 +146,7 @@ namespace Wabbajack.Server
|
|||||||
app.UseService<Watchdog>();
|
app.UseService<Watchdog>();
|
||||||
app.UseService<DiscordFrontend>();
|
app.UseService<DiscordFrontend>();
|
||||||
app.UseService<AuthoredFilesCleanup>();
|
app.UseService<AuthoredFilesCleanup>();
|
||||||
|
app.UseService<MetricsKeyCache>();
|
||||||
|
|
||||||
app.Use(next =>
|
app.Use(next =>
|
||||||
{
|
{
|
||||||
|
@ -45,6 +45,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="sheo_quotes.txt" />
|
<None Remove="sheo_quotes.txt" />
|
||||||
<EmbeddedResource Include="sheo_quotes.txt" />
|
<EmbeddedResource Include="sheo_quotes.txt" />
|
||||||
|
<None Remove="Controllers\Templates\TotalListTemplate.html" />
|
||||||
|
<EmbeddedResource Include="Controllers\Templates\TotalListTemplate.html" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -119,7 +119,7 @@ namespace Wabbajack.Test
|
|||||||
{
|
{
|
||||||
Assert.Equal(HTMLInterface.PermissionValue.No, await HTMLInterface.GetUploadPermissions(Game.SkyrimSpecialEdition, 266));
|
Assert.Equal(HTMLInterface.PermissionValue.No, await HTMLInterface.GetUploadPermissions(Game.SkyrimSpecialEdition, 266));
|
||||||
Assert.Equal(HTMLInterface.PermissionValue.Yes, await HTMLInterface.GetUploadPermissions(Game.SkyrimSpecialEdition, 1137));
|
Assert.Equal(HTMLInterface.PermissionValue.Yes, await HTMLInterface.GetUploadPermissions(Game.SkyrimSpecialEdition, 1137));
|
||||||
Assert.Equal(HTMLInterface.PermissionValue.Hidden, await HTMLInterface.GetUploadPermissions(Game.SkyrimSpecialEdition, 34604));
|
Assert.Contains(await HTMLInterface.GetUploadPermissions(Game.SkyrimSpecialEdition, 34604), new[]{HTMLInterface.PermissionValue.Hidden, HTMLInterface.PermissionValue.NotFound});
|
||||||
Assert.Equal(HTMLInterface.PermissionValue.NotFound, await HTMLInterface.GetUploadPermissions(Game.SkyrimSpecialEdition, 24287));
|
Assert.Equal(HTMLInterface.PermissionValue.NotFound, await HTMLInterface.GetUploadPermissions(Game.SkyrimSpecialEdition, 24287));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,9 @@ namespace Wabbajack.Test
|
|||||||
Assert.Equal(Hash.FromBase64("eSIyd+KOG3s="), await filename.Path.FileHashAsync());
|
Assert.Equal(Hash.FromBase64("eSIyd+KOG3s="), await filename.Path.FileHashAsync());
|
||||||
|
|
||||||
Assert.Equal("Cheese for Everyone!", await filename.Path.ReadAllTextAsync());
|
Assert.Equal("Cheese for Everyone!", await filename.Path.ReadAllTextAsync());
|
||||||
|
|
||||||
|
var newState = (AbstractDownloadState)new GoogleDriveDownloader.State("1Q_CdeYJStfoTZFLZ79RRVkxI2c_cG0dg");
|
||||||
|
Assert.True(await newState.Verify(new Archive(newState) {Size = 0}));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
Loading…
Reference in New Issue
Block a user