using System; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.LogParsing; using StardewModdingAPI.Web.Framework.LogParsing.Models; using StardewModdingAPI.Web.Framework.Storage; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers { /// Provides a web UI and API for parsing SMAPI log files. internal class LogParserController : Controller { /********* ** Fields *********/ /// Provides access to raw data storage. private readonly IStorageProvider Storage; /********* ** Public methods *********/ /*** ** Constructor ***/ /// Construct an instance. /// Provides access to raw data storage. public LogParserController(IStorageProvider storage) { this.Storage = storage; } /*** ** Web UI ***/ /// Render the log parser UI. /// The stored file ID. /// How to render the log view. /// Whether to reset the log expiry. [HttpGet] [Route("log")] [Route("log/{id}")] public async Task Index(string? id = null, LogViewFormat format = LogViewFormat.Default, bool renew = false) { // fresh page if (string.IsNullOrWhiteSpace(id)) return this.View("Index", this.GetModel(id)); // fetch log StoredFileInfo file = await this.Storage.GetAsync(id, renew); // render view switch (format) { case LogViewFormat.Default: case LogViewFormat.RawView: { ParsedLog log = file.Success ? new LogParser().Parse(file.Content) : new ParsedLog { IsValid = false, Error = file.Error }; return this.View("Index", this.GetModel(id, uploadWarning: file.Warning, expiry: file.Expiry).SetResult(log, showRaw: format == LogViewFormat.RawView)); } case LogViewFormat.RawDownload: { string content = file.Error ?? file.Content; return this.File(Encoding.UTF8.GetBytes(content), "plain/text", $"SMAPI log ({id}).txt"); } default: throw new InvalidOperationException($"Unknown log view format '{format}'."); } } /*** ** JSON ***/ /// Save raw log data. [HttpPost, AllowLargePosts] [Route("log")] public async Task PostAsync() { // get raw log text string? input = this.Request.Form["input"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(input)) return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty.")); // upload log UploadResult uploadResult = await this.Storage.SaveAsync(input); if (!uploadResult.Succeeded) return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError)); // redirect to view return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID })); } /********* ** Private methods *********/ /// Build a log parser model. /// The stored file ID. /// When the uploaded file will no longer be available. /// A non-blocking warning while uploading the log. /// An error which occurred while uploading the log. private LogParserModel GetModel(string? pasteID, DateTime? expiry = null, string? uploadWarning = null, string? uploadError = null) { Platform? platform = this.DetectClientPlatform(); return new LogParserModel(pasteID, platform) { UploadWarning = uploadWarning, UploadError = uploadError, Expiry = expiry }; } /// Detect the viewer's OS. /// Returns the viewer OS if known, else null. private Platform? DetectClientPlatform() { string userAgent = this.Request.Headers["User-Agent"]; switch (userAgent) { case string ua when ua.Contains("Windows"): return Platform.Windows; case string ua when ua.Contains("Android"): // check for Android before Linux because Android user agents also contain Linux return Platform.Android; case string ua when ua.Contains("Linux"): return Platform.Linux; case string ua when ua.Contains("Mac"): return Platform.Mac; default: return null; } } } }