From 6abddc68bfe3b086c62c799f81bccc642472aa5f Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Fri, 8 May 2020 21:56:06 -0600 Subject: [PATCH] Rewriting the build server (mostly from scratch) --- .../Controllers/AControllerBase.cs | 2 - .../Models/Jobs/GetNexusUpdatesJob.cs | 2 - Wabbajack.Server/AppSettings.cs | 34 ++++ Wabbajack.Server/Controllers/Metrics.cs | 50 +++++ Wabbajack.Server/Controllers/NexusCache.cs | 98 ++++++++++ Wabbajack.Server/DTOs/Metric.cs | 12 ++ Wabbajack.Server/DTOs/NexusUpdateEntry.cs | 14 ++ Wabbajack.Server/DataLayer/Metrics.cs | 15 ++ Wabbajack.Server/DataLayer/Nexus.cs | 93 ++++++++++ Wabbajack.Server/DataLayer/SqlService.cs | 24 +++ Wabbajack.Server/GlobalInformation.cs | 12 ++ Wabbajack.Server/Services/NexusPoll.cs | 171 ++++++++++++++++++ Wabbajack.Server/Startup.cs | 130 +++++++++++++ Wabbajack.Server/appsettings.json | 22 +++ .../public/WABBAJACK_TEST_FILE.txt | 1 + Wabbajack.sln | 10 + 16 files changed, 686 insertions(+), 4 deletions(-) create mode 100644 Wabbajack.Server/AppSettings.cs create mode 100644 Wabbajack.Server/Controllers/Metrics.cs create mode 100644 Wabbajack.Server/Controllers/NexusCache.cs create mode 100644 Wabbajack.Server/DTOs/Metric.cs create mode 100644 Wabbajack.Server/DTOs/NexusUpdateEntry.cs create mode 100644 Wabbajack.Server/DataLayer/Metrics.cs create mode 100644 Wabbajack.Server/DataLayer/Nexus.cs create mode 100644 Wabbajack.Server/DataLayer/SqlService.cs create mode 100644 Wabbajack.Server/GlobalInformation.cs create mode 100644 Wabbajack.Server/Services/NexusPoll.cs create mode 100644 Wabbajack.Server/Startup.cs create mode 100644 Wabbajack.Server/appsettings.json create mode 100644 Wabbajack.Server/public/WABBAJACK_TEST_FILE.txt diff --git a/Wabbajack.BuildServer/Controllers/AControllerBase.cs b/Wabbajack.BuildServer/Controllers/AControllerBase.cs index c0bafbe7..4c6d52cb 100644 --- a/Wabbajack.BuildServer/Controllers/AControllerBase.cs +++ b/Wabbajack.BuildServer/Controllers/AControllerBase.cs @@ -32,7 +32,5 @@ namespace Wabbajack.BuildServer.Controllers Timestamp = DateTime.UtcNow }); } - - } } diff --git a/Wabbajack.BuildServer/Models/Jobs/GetNexusUpdatesJob.cs b/Wabbajack.BuildServer/Models/Jobs/GetNexusUpdatesJob.cs index ee5621e6..b38fb5e8 100644 --- a/Wabbajack.BuildServer/Models/Jobs/GetNexusUpdatesJob.cs +++ b/Wabbajack.BuildServer/Models/Jobs/GetNexusUpdatesJob.cs @@ -118,7 +118,5 @@ namespace Wabbajack.BuildServer.Models.Jobs LastNexusSync = DateTime.Now; return updated; } - - } } diff --git a/Wabbajack.Server/AppSettings.cs b/Wabbajack.Server/AppSettings.cs new file mode 100644 index 00000000..85766005 --- /dev/null +++ b/Wabbajack.Server/AppSettings.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Configuration; +using Wabbajack.Common; + +namespace Wabbajack.BuildServer +{ + public class AppSettings + { + public AppSettings(IConfiguration config) + { + config.Bind("WabbajackSettings", this); + } + + public string DownloadDir { get; set; } + public AbsolutePath DownloadPath => (AbsolutePath)DownloadDir; + public string ArchiveDir { get; set; } + public AbsolutePath ArchivePath => (AbsolutePath)ArchiveDir; + + public string TempFolder { get; set; } + + public AbsolutePath TempPath => (AbsolutePath)TempFolder; + + public bool JobScheduler { get; set; } + public bool JobRunner { get; set; } + + public bool RunFrontEndJobs { get; set; } + public bool RunBackEndJobs { get; set; } + + public string BunnyCDN_User { get; set; } + public string BunnyCDN_Password { get; set; } + public string SqlConnection { get; set; } + + public int MaxJobs { get; set; } = 2; + } +} diff --git a/Wabbajack.Server/Controllers/Metrics.cs b/Wabbajack.Server/Controllers/Metrics.cs new file mode 100644 index 00000000..ec84c46f --- /dev/null +++ b/Wabbajack.Server/Controllers/Metrics.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Wabbajack.Common; +using Wabbajack.Server.DataLayer; +using Wabbajack.Server.DTOs; +using WebSocketSharp; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace Wabbajack.BuildServer.Controllers +{ + [ApiController] + [Route("/metrics")] + public class MetricsController : ControllerBase + { + private SqlService _sql; + private ILogger _logger; + + public MetricsController(ILogger logger, SqlService sql) + { + _sql = sql; + _logger = logger; + } + + [HttpGet] + [Route("{Subject}/{Value}")] + public async Task LogMetricAsync(string Subject, string Value) + { + var date = DateTime.UtcNow; + await Log(date, Subject, Value, Request.Headers[Consts.MetricsKeyHeader].FirstOrDefault()); + return new Result { Timestamp = date}; + } + + private async Task Log(DateTime timestamp, string action, string subject, string metricsKey = null) + { + _logger.Log(LogLevel.Information, $"Log - {timestamp} {action} {subject} {metricsKey}"); + await _sql.IngestMetric(new Metric + { + Timestamp = timestamp, Action = action, Subject = subject, MetricsKey = metricsKey + }); + } + + public class Result + { + public DateTime Timestamp { get; set; } + } + } +} diff --git a/Wabbajack.Server/Controllers/NexusCache.cs b/Wabbajack.Server/Controllers/NexusCache.cs new file mode 100644 index 00000000..c1320fc3 --- /dev/null +++ b/Wabbajack.Server/Controllers/NexusCache.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AngleSharp.Io; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +using Wabbajack.Common; +using Wabbajack.Lib; +using Wabbajack.Lib.NexusApi; +using Wabbajack.Server.DataLayer; + +namespace Wabbajack.BuildServer.Controllers +{ + //[Authorize] + [ApiController] + [Route("/v1/games/")] + public class NexusCache : ControllerBase + { + private AppSettings _settings; + private static long CachedCount = 0; + private static long ForwardCount = 0; + private SqlService _sql; + private ILogger _logger; + + public NexusCache(ILogger logger, SqlService sql, AppSettings settings) + { + _settings = settings; + _sql = sql; + _logger = logger; + } + + /// + /// Looks up the mod details for a given Gamename/ModId pair. If the entry is not found in the cache it will + /// be requested from the server (using the caller's Nexus API key if provided). + /// + /// + /// The Nexus game name + /// The Nexus mod id + /// A Mod Info result + [HttpGet] + [Route("{GameName}/mods/{ModId}.json")] + public async Task GetModInfo(string GameName, long ModId) + { + var game = GameRegistry.GetByFuzzyName(GameName).Game; + var result = await _sql.GetNexusModInfoString(game, ModId); + + string method = "CACHED"; + if (result == null) + { + var api = await NexusApiClient.Get(Request.Headers["apikey"].FirstOrDefault()); + result = await api.GetModInfo(game, ModId, false); + await _sql.AddNexusModInfo(game, ModId, DateTime.UtcNow, result); + + + method = "NOT_CACHED"; + Interlocked.Increment(ref ForwardCount); + } + else + { + Interlocked.Increment(ref CachedCount); + } + + Response.Headers.Add("x-cache-result", method); + return result; + } + + [HttpGet] + [Route("{GameName}/mods/{ModId}/files.json")] + public async Task GetModFiles(string GameName, long ModId) + { + _logger.Log(LogLevel.Information, $"{GameName} {ModId}"); + var game = GameRegistry.GetByFuzzyName(GameName).Game; + var result = await _sql.GetModFiles(game, ModId); + + string method = "CACHED"; + if (result == null) + { + var api = await NexusApiClient.Get(Request.Headers["apikey"].FirstOrDefault()); + result = await api.GetModFiles(game, ModId, false); + await _sql.AddNexusModFiles(game, ModId, DateTime.UtcNow, result); + + method = "NOT_CACHED"; + Interlocked.Increment(ref ForwardCount); + } + else + { + Interlocked.Increment(ref CachedCount); + } + Response.Headers.Add("x-cache-result", method); + return result; + } + } +} diff --git a/Wabbajack.Server/DTOs/Metric.cs b/Wabbajack.Server/DTOs/Metric.cs new file mode 100644 index 00000000..2c47c7bc --- /dev/null +++ b/Wabbajack.Server/DTOs/Metric.cs @@ -0,0 +1,12 @@ +using System; + +namespace Wabbajack.Server.DTOs +{ + public class Metric + { + public DateTime Timestamp { get; set; } + public string Action { get; set; } + public string Subject { get; set; } + public string MetricsKey { get; set; } + } +} diff --git a/Wabbajack.Server/DTOs/NexusUpdateEntry.cs b/Wabbajack.Server/DTOs/NexusUpdateEntry.cs new file mode 100644 index 00000000..910280d2 --- /dev/null +++ b/Wabbajack.Server/DTOs/NexusUpdateEntry.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Wabbajack.Server.DTOs +{ + public class NexusUpdateEntry + { + [JsonProperty("mod_id")] + public long ModId { get; set; } + [JsonProperty("latest_file_update")] + public long LatestFileUpdate { get; set; } + [JsonProperty("latest_mod_activity")] + public long LastestModActivity { get; set; } + } +} diff --git a/Wabbajack.Server/DataLayer/Metrics.cs b/Wabbajack.Server/DataLayer/Metrics.cs new file mode 100644 index 00000000..ea308081 --- /dev/null +++ b/Wabbajack.Server/DataLayer/Metrics.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Dapper; +using Wabbajack.Server.DTOs; + +namespace Wabbajack.Server.DataLayer +{ + public partial class SqlService + { + public async Task IngestMetric(Metric metric) + { + await using var conn = await Open(); + await conn.ExecuteAsync(@"INSERT INTO dbo.Metrics (Timestamp, Action, Subject, MetricsKey) VALUES (@Timestamp, @Action, @Subject, @MetricsKey)", metric); + } + } +} diff --git a/Wabbajack.Server/DataLayer/Nexus.cs b/Wabbajack.Server/DataLayer/Nexus.cs new file mode 100644 index 00000000..6ff8bf75 --- /dev/null +++ b/Wabbajack.Server/DataLayer/Nexus.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using Dapper; +using Newtonsoft.Json; +using Wabbajack.Common; +using Wabbajack.Lib.NexusApi; + +namespace Wabbajack.Server.DataLayer +{ + /// + /// SQL routines that read/write cached information from the Nexus + /// + public partial class SqlService + { + public async Task DeleteNexusModInfosUpdatedBeforeDate(Game game, long modId, DateTime date) + { + await using var conn = await Open(); + var deleted = await conn.ExecuteScalarAsync( + @"DELETE FROM dbo.NexusModInfos WHERE Game = @Game AND ModID = @ModId AND LastChecked < @Date + SELECT @@ROWCOUNT AS Deleted", + new {Game = game.MetaData().NexusGameId, ModId = modId, @Date = date}); + return deleted; + } + + public async Task DeleteNexusModFilesUpdatedBeforeDate(Game game, long modId, DateTime date) + { + await using var conn = await Open(); + var deleted = await conn.ExecuteScalarAsync( + @"DELETE FROM dbo.NexusModFiles WHERE Game = @Game AND ModID = @ModId AND LastChecked < @Date + SELECT @@ROWCOUNT AS Deleted", + new {Game = game.MetaData().NexusGameId, ModId = modId, Date = date}); + return deleted; + } + + public async Task GetNexusModInfoString(Game game, long modId) + { + await using var conn = await Open(); + var result = await conn.QueryFirstOrDefaultAsync( + "SELECT Data FROM dbo.NexusModInfos WHERE Game = @Game AND @ModId = ModId", + new {Game = game.MetaData().NexusGameId, ModId = modId}); + return result == null ? null : JsonConvert.DeserializeObject(result); + } + + public async Task AddNexusModInfo(Game game, long modId, DateTime lastCheckedUtc, ModInfo data) + { + await using var conn = await Open(); + + await conn.ExecuteAsync( + @"MERGE dbo.NexusModInfos AS Target + USING (SELECT @Game Game, @ModId ModId, @LastChecked LastChecked, @Data Data) AS Source + ON Target.Game = Source.Game AND Target.ModId = Source.ModId + WHEN MATCHED THEN UPDATE SET Target.Data = @Data, Target.LastChecked = @LastChecked + WHEN NOT MATCHED THEN INSERT (Game, ModId, LastChecked, Data) VALUES (@Game, @ModId, @LastChecked, @Data);", + new + { + Game = game.MetaData().NexusGameId, + ModId = modId, + LastChecked = lastCheckedUtc, + Data = JsonConvert.SerializeObject(data) + }); + + } + + public async Task AddNexusModFiles(Game game, long modId, DateTime lastCheckedUtc, NexusApiClient.GetModFilesResponse data) + { + await using var conn = await Open(); + + await conn.ExecuteAsync( + @"MERGE dbo.NexusModFiles AS Target + USING (SELECT @Game Game, @ModId ModId, @LastChecked LastChecked, @Data Data) AS Source + ON Target.Game = Source.Game AND Target.ModId = Source.ModId + WHEN MATCHED THEN UPDATE SET Target.Data = @Data, Target.LastChecked = @LastChecked + WHEN NOT MATCHED THEN INSERT (Game, ModId, LastChecked, Data) VALUES (@Game, @ModId, @LastChecked, @Data);", + new + { + Game = game.MetaData().NexusGameId, + ModId = modId, + LastChecked = lastCheckedUtc, + Data = JsonConvert.SerializeObject(data) + }); + + } + + public async Task GetModFiles(Game game, long modId) + { + await using var conn = await Open(); + var result = await conn.QueryFirstOrDefaultAsync( + "SELECT Data FROM dbo.NexusModFiles WHERE Game = @Game AND @ModId = ModId", + new {Game = game.MetaData().NexusGameId, ModId = modId}); + return result == null ? null : JsonConvert.DeserializeObject(result); + } + } +} diff --git a/Wabbajack.Server/DataLayer/SqlService.cs b/Wabbajack.Server/DataLayer/SqlService.cs new file mode 100644 index 00000000..8965a682 --- /dev/null +++ b/Wabbajack.Server/DataLayer/SqlService.cs @@ -0,0 +1,24 @@ +using System.Data.SqlClient; +using System.Threading.Tasks; +using Wabbajack.BuildServer; + +namespace Wabbajack.Server.DataLayer +{ + public partial class SqlService + { + private AppSettings _settings; + + public SqlService(AppSettings settings) + { + _settings = settings; + + } + + public async Task Open() + { + var conn = new SqlConnection(_settings.SqlConnection); + await conn.OpenAsync(); + return conn; + } + } +} diff --git a/Wabbajack.Server/GlobalInformation.cs b/Wabbajack.Server/GlobalInformation.cs new file mode 100644 index 00000000..d1eb3705 --- /dev/null +++ b/Wabbajack.Server/GlobalInformation.cs @@ -0,0 +1,12 @@ +using System; + +namespace Wabbajack.Server +{ + public class GlobalInformation + { + public TimeSpan NexusRSSPollRate = TimeSpan.FromMinutes(1); + public TimeSpan NexusAPIPollRate = TimeSpan.FromHours(2); + public DateTime LastNexusSyncUTC { get; set; } + public TimeSpan TimeSinceLastNexusSync => DateTime.UtcNow - LastNexusSyncUTC; + } +} diff --git a/Wabbajack.Server/Services/NexusPoll.cs b/Wabbajack.Server/Services/NexusPoll.cs new file mode 100644 index 00000000..4d776af3 --- /dev/null +++ b/Wabbajack.Server/Services/NexusPoll.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging; +using Wabbajack.BuildServer; +using Wabbajack.Common; +using Wabbajack.Lib.NexusApi; +using Wabbajack.Server.DataLayer; +using Wabbajack.Server.DTOs; + +namespace Wabbajack.Server.Services +{ + public class NexusPoll + { + private SqlService _sql; + private AppSettings _settings; + private GlobalInformation _globalInformation; + private ILogger _logger; + + public NexusPoll(ILogger logger, AppSettings settings, SqlService service, GlobalInformation globalInformation) + { + _sql = service; + _settings = settings; + _globalInformation = globalInformation; + _logger = logger; + } + + public async Task UpdateNexusCacheRSS() + { + using var _ = _logger.BeginScope("Nexus Update via RSS"); + _logger.Log(LogLevel.Information, "Starting"); + + var results = await NexusUpdatesFeeds.GetUpdates(); + NexusApiClient client = null; + long updated = 0; + foreach (var result in results) + { + try + { + var purgedMods = + await _sql.DeleteNexusModFilesUpdatedBeforeDate(result.Game, result.ModId, result.TimeStamp); + var purgedFiles = + await _sql.DeleteNexusModInfosUpdatedBeforeDate(result.Game, result.ModId, result.TimeStamp); + + var totalPurged = purgedFiles + purgedMods; + if (totalPurged > 0) + _logger.Log(LogLevel.Information, $"Purged {totalPurged} cache items"); + + if (await _sql.GetNexusModInfoString(result.Game, result.ModId) != null) continue; + + // Lazily create the client + client ??= await NexusApiClient.Get(); + + // Cache the info + var files = await client.GetModFiles(result.Game, result.ModId, false); + await _sql.AddNexusModFiles(result.Game, result.ModId, result.TimeStamp, files); + + var modInfo = await client.GetModInfo(result.Game, result.ModId, useCache: false); + await _sql.AddNexusModInfo(result.Game, result.ModId, result.TimeStamp, modInfo); + updated++; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed Nexus update for {result.Game} - {result.ModId} - {result.TimeStamp}"); + } + + } + + if (updated > 0) + _logger.Log(LogLevel.Information, $"Primed {updated} nexus cache entries"); + + _globalInformation.LastNexusSyncUTC = DateTime.UtcNow; + } + + public async Task UpdateNexusCacheAPI() + { + using var _ = _logger.BeginScope("Nexus Update via API"); + _logger.Log(LogLevel.Information, "Starting"); + var api = await NexusApiClient.Get(); + + var gameTasks = GameRegistry.Games.Values + .Where(game => game.NexusName != null) + .Select(async game => + { + var mods = await api.Get>( + $"https://api.nexusmods.com/v1/games/{game.NexusName}/mods/updated.json?period=1m"); + + return (game, mods); + }) + .Select(async rTask => + { + var (game, mods) = await rTask; + return mods.Select(mod => new { game = game, mod = mod }); + }).ToList(); + + _logger.Log(LogLevel.Information, $"Getting update list for {gameTasks.Count} games"); + + var purge = (await Task.WhenAll(gameTasks)) + .SelectMany(i => i) + .ToList(); + + _logger.Log(LogLevel.Information, $"Found {purge.Count} updated mods in the last month"); + using var queue = new WorkQueue(); + var collected = purge.Select(d => + { + var a = d.mod.LatestFileUpdate.AsUnixTime(); + // Mod activity could hide files + var b = d.mod.LastestModActivity.AsUnixTime(); + + return new {Game = d.game.Game, Date = (a > b ? a : b), ModId = d.mod.ModId}; + }); + + var purged = await collected.PMap(queue, async t => + { + var resultA = await _sql.DeleteNexusModInfosUpdatedBeforeDate(t.Game, t.ModId, t.Date); + var resultB = await _sql.DeleteNexusModFilesUpdatedBeforeDate(t.Game, t.ModId, t.Date); + return resultA + resultB; + }); + + _logger.Log(LogLevel.Information, $"Purged {purged.Sum()} cache entries"); + } + + public void Start() + { + Task.Run(async () => + { + while (true) + { + try + { + await UpdateNexusCacheRSS(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error polling from Nexus"); + } + await Task.Delay(_globalInformation.NexusRSSPollRate); + } + }); + + Task.Run(async () => + { + while (true) + { + try + { + await UpdateNexusCacheAPI(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting API feed from Nexus"); + } + + await Task.Delay(_globalInformation.NexusAPIPollRate); + } + }); + } + } + + public static class NexusPollExtensions + { + public static void UseNexusPoll(this IApplicationBuilder b) + { + var poll = (NexusPoll)b.ApplicationServices.GetService(typeof(NexusPoll)); + poll.Start(); + } + + } +} diff --git a/Wabbajack.Server/Startup.cs b/Wabbajack.Server/Startup.cs new file mode 100644 index 00000000..e82ed858 --- /dev/null +++ b/Wabbajack.Server/Startup.cs @@ -0,0 +1,130 @@ +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json; +using Wabbajack.BuildServer; +using Wabbajack.Server.DataLayer; +using Wabbajack.Server.Services; + +namespace Wabbajack.Server +{ + public class TestStartup : Startup + { + public TestStartup(IConfiguration configuration) : base(configuration) + { + } + } + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo {Title = "Wabbajack Build API", Version = "v1"}); + }); + + services.Configure(x => + { + x.ValueLengthLimit = int.MaxValue; + x.MultipartBodyLengthLimit = int.MaxValue; + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddMvc(); + services.AddControllers() + .AddNewtonsoftJson(o => + { + + o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }); + + + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + if (!(this is TestStartup)) + app.UseHttpsRedirection(); + + app.UseDeveloperExceptionPage(); + + var provider = new FileExtensionContentTypeProvider(); + provider.Mappings[".rar"] = "application/x-rar-compressed"; + provider.Mappings[".7z"] = "application/x-7z-compressed"; + provider.Mappings[".zip"] = "application/zip"; + provider.Mappings[".wabbajack"] = "application/zip"; + app.UseStaticFiles(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Wabbajack Build API"); + c.RoutePrefix = string.Empty; + }); + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + app.UseNexusPoll(); + + app.Use(next => + { + return async context => + { + var stopWatch = new Stopwatch(); + stopWatch.Start(); + context.Response.OnStarting(() => + { + stopWatch.Stop(); + var headers = context.Response.Headers; + headers.Add("Access-Control-Allow-Origin", "*"); + headers.Add("Access-Control-Allow-Methods", "POST, GET"); + headers.Add("Access-Control-Allow-Headers", "Accept, Origin, Content-type"); + headers.Add("X-ResponseTime-Ms", stopWatch.ElapsedMilliseconds.ToString()); + return Task.CompletedTask; + }); + await next(context); + }; + }); + + app.UseFileServer(new FileServerOptions + { + FileProvider = new PhysicalFileProvider( + Path.Combine(Directory.GetCurrentDirectory(), "public")), + StaticFileOptions = {ServeUnknownFileTypes = true}, + + }); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/Wabbajack.Server/appsettings.json b/Wabbajack.Server/appsettings.json new file mode 100644 index 00000000..a4fd70fa --- /dev/null +++ b/Wabbajack.Server/appsettings.json @@ -0,0 +1,22 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "WabbajackSettings": { + "DownloadDir": "c:\\tmp\\downloads", + "ArchiveDir": "w:\\archives", + "TempFolder": "c:\\tmp", + "JobRunner": true, + "JobScheduler": false, + "RunFrontEndJobs": true, + "RunBackEndJobs": true, + "BunnyCDN_User": "wabbajackcdn", + "BunnyCDN_Password": "XXXX", + "SQLConnection": "Data Source=.\\SQLEXPRESS;Integrated Security=True;Initial Catalog=wabbajack_prod;MultipleActiveResultSets=true" + }, + "AllowedHosts": "*" +} diff --git a/Wabbajack.Server/public/WABBAJACK_TEST_FILE.txt b/Wabbajack.Server/public/WABBAJACK_TEST_FILE.txt new file mode 100644 index 00000000..bc303547 --- /dev/null +++ b/Wabbajack.Server/public/WABBAJACK_TEST_FILE.txt @@ -0,0 +1 @@ +Cheese for Everyone! \ No newline at end of file diff --git a/Wabbajack.sln b/Wabbajack.sln index 745a4294..e9fe553c 100644 --- a/Wabbajack.sln +++ b/Wabbajack.sln @@ -44,6 +44,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.App.Test", "Wabba EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.BuildServer.Test", "Wabbajack.BuildServer.Test\Wabbajack.BuildServer.Test.csproj", "{160D3A0F-68E1-4AFF-8625-E5E0FFBB2058}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Server", "Wabbajack.Server\Wabbajack.Server.csproj", "{3E11B700-8405-433D-BF47-6C356087A7C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -159,6 +161,14 @@ Global {160D3A0F-68E1-4AFF-8625-E5E0FFBB2058}.Release|Any CPU.Build.0 = Release|Any CPU {160D3A0F-68E1-4AFF-8625-E5E0FFBB2058}.Release|x64.ActiveCfg = Release|Any CPU {160D3A0F-68E1-4AFF-8625-E5E0FFBB2058}.Release|x64.Build.0 = Release|Any CPU + {3E11B700-8405-433D-BF47-6C356087A7C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E11B700-8405-433D-BF47-6C356087A7C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E11B700-8405-433D-BF47-6C356087A7C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {3E11B700-8405-433D-BF47-6C356087A7C2}.Debug|x64.Build.0 = Debug|Any CPU + {3E11B700-8405-433D-BF47-6C356087A7C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E11B700-8405-433D-BF47-6C356087A7C2}.Release|Any CPU.Build.0 = Release|Any CPU + {3E11B700-8405-433D-BF47-6C356087A7C2}.Release|x64.ActiveCfg = Release|Any CPU + {3E11B700-8405-433D-BF47-6C356087A7C2}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE