using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using FluentFTP; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Nettle; using Wabbajack.BuildServer.Model.Models; using Wabbajack.BuildServer.Models; using Wabbajack.BuildServer.Models.JobQueue; using Wabbajack.BuildServer.Models.Jobs; using Wabbajack.Common; using Wabbajack.Lib; using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.ModListRegistry; namespace Wabbajack.BuildServer.Controllers { [ApiController] [Route("/lists")] public class ListValidation : AControllerBase { enum ArchiveStatus { Valid, InValid, Updating, Updated, } public ListValidation(ILogger logger, SqlService sql, AppSettings settings) : base(logger, sql) { _updater = new ModlistUpdater(null, sql, settings); _settings = settings; } public async Task> GetSummaries() { var data = await SQL.GetValidationData(); using var queue = new WorkQueue(); var results = await data.ModLists.PMap(queue, async list => { var (metadata, modList) = list; var archives = await modList.Archives.PMap(queue, async archive => { var (_, result) = ValidateArchive(data, archive); if (result == ArchiveStatus.InValid) { return await TryToFix(data, archive); } return (archive, result); }); var failedCount = archives.Count(f => f.Item2 == ArchiveStatus.InValid); var passCount = archives.Count(f => f.Item2 == ArchiveStatus.Valid || f.Item2 == ArchiveStatus.Updated); var updatingCount = archives.Count(f => f.Item2 == ArchiveStatus.Updating); var summary = new ModListSummary { Checked = DateTime.UtcNow, Failed = failedCount, Passed = passCount, Updating = updatingCount, MachineURL = metadata.Links.MachineURL, Name = metadata.Title, }; var detailed = new DetailedStatus { Name = metadata.Title, Checked = DateTime.UtcNow, DownloadMetaData = metadata.DownloadMetadata, HasFailures = failedCount > 0, MachineName = metadata.Links.MachineURL, Archives = archives.Select(a => new DetailedStatusItem { Archive = a.Item1, IsFailing = a.Item2 == ArchiveStatus.InValid || a.Item2 == ArchiveStatus.Updating }).ToList() }; return (summary, detailed); }); return results; } private static (Archive archive, ArchiveStatus) ValidateArchive(SqlService.ValidationData data, Archive archive) { switch (archive.State) { case NexusDownloader.State nexusState when data.NexusFiles.Contains(( nexusState.Game.MetaData().NexusGameId, nexusState.ModID, nexusState.FileID)): return (archive, ArchiveStatus.Valid); case NexusDownloader.State nexusState: return (archive, ArchiveStatus.InValid); case ManualDownloader.State _: return (archive, ArchiveStatus.Valid); default: { if (data.ArchiveStatus.TryGetValue((archive.State.PrimaryKeyString, archive.Hash), out bool isValid)) { return isValid ? (archive, ArchiveStatus.Valid) : (archive, ArchiveStatus.InValid); } return (archive, ArchiveStatus.InValid); } } } private static AsyncLock _findPatchLock = new AsyncLock(); private async Task<(Archive, ArchiveStatus)> TryToFix(SqlService.ValidationData data, Archive archive) { using var _ = await _findPatchLock.Wait(); var result = await _updater.GetAlternative(archive.Hash.ToHex()); return result switch { OkResult ok => (archive, ArchiveStatus.Updated), AcceptedResult accept => (archive, ArchiveStatus.Updating), _ => (archive, ArchiveStatus.InValid) }; } [HttpGet] [Route("status.json")] public async Task> HandleGetLists() { return (await GetSummaries()).Select(d => d.Summary); } private static readonly Func HandleGetRssFeedTemplate = NettleEngine.GetCompiler().Compile(@" {{lst.Name}} - Broken Mods http://build.wabbajack.org/status/{{lst.Name}}.html These are mods that are broken and need updating {{ each $.failed }} {{$.Archive.Name}} {{$.Archive.Hash}} {{$.Archive.State.PrimaryKeyString}} {{$.Archive.Name}} {{/each}} "); [HttpGet] [Route("status/{Name}/broken.rss")] public async Task HandleGetRSSFeed(string Name) { var lst = await DetailedStatus(Name); var response = HandleGetRssFeedTemplate(new { lst, failed = lst.Archives.Where(a => a.IsFailing).ToList(), passed = lst.Archives.Where(a => !a.IsFailing).ToList() }); return new ContentResult { ContentType = "application/rss+xml", StatusCode = (int) HttpStatusCode.OK, Content = response }; } private static readonly Func HandleGetListTemplate = NettleEngine.GetCompiler().Compile(@"

{{lst.Name}} - {{lst.Checked}} - {{ago}}min ago

Failed ({{failed.Count}}):

    {{each $.failed }}
  • {{$.Archive.Name}}
  • {{/each}}

Passed ({{passed.Count}}):

    {{each $.passed }}
  • {{$.Archive.Name}}
  • {{/each}}
"); private AppSettings _settings; private ModlistUpdater _updater; [HttpGet] [Route("status/{Name}.html")] public async Task HandleGetListHtml(string Name) { var lst = await DetailedStatus(Name); var response = HandleGetListTemplate(new { lst, ago = (DateTime.UtcNow - lst.Checked).TotalMinutes, failed = lst.Archives.Where(a => a.IsFailing).ToList(), passed = lst.Archives.Where(a => !a.IsFailing).ToList() }); return new ContentResult { ContentType = "text/html", StatusCode = (int) HttpStatusCode.OK, Content = response }; } [HttpGet] [Route("status/{Name}.json")] public async Task HandleGetListJson(string Name) { return Ok((await DetailedStatus(Name)).ToJson()); } private async Task DetailedStatus(string Name) { return (await GetSummaries()) .Select(d => d.Detailed) .FirstOrDefault(d => d.MachineName == Name); } } }