diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2019-12-03 21:21:28 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2019-12-03 21:21:28 -0500 |
commit | 2b1f607d41b3d4d071c0db0671dbc99b6982909f (patch) | |
tree | a854b7465c73a38c12b3b841515e39d9154217a7 /src/SMAPI.Web | |
parent | 0aad3f545af854619c641dc8b57ac0cf12971c8e (diff) | |
download | SMAPI-2b1f607d41b3d4d071c0db0671dbc99b6982909f.tar.gz SMAPI-2b1f607d41b3d4d071c0db0671dbc99b6982909f.tar.bz2 SMAPI-2b1f607d41b3d4d071c0db0671dbc99b6982909f.zip |
encapsulate file storage, also handle Pastebin rate limits in JSON validator
Diffstat (limited to 'src/SMAPI.Web')
-rw-r--r-- | src/SMAPI.Web/Controllers/JsonValidatorController.cs | 58 | ||||
-rw-r--r-- | src/SMAPI.Web/Controllers/LogParserController.cs | 178 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs | 6 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs | 2 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/Storage/IStorageProvider.cs | 19 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/Storage/StorageProvider.cs | 147 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs | 23 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/Storage/UploadResult.cs | 33 | ||||
-rw-r--r-- | src/SMAPI.Web/Startup.cs | 10 | ||||
-rw-r--r-- | src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs | 19 | ||||
-rw-r--r-- | src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 15 | ||||
-rw-r--r-- | src/SMAPI.Web/appsettings.json | 2 | ||||
-rw-r--r-- | src/SMAPI.Web/wwwroot/Content/css/json-validator.css | 6 |
13 files changed, 306 insertions, 212 deletions
diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 40599abc..830fe839 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -9,8 +9,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; using StardewModdingAPI.Web.Framework; -using StardewModdingAPI.Web.Framework.Clients.Pastebin; -using StardewModdingAPI.Web.Framework.Compression; +using StardewModdingAPI.Web.Framework.Storage; using StardewModdingAPI.Web.ViewModels.JsonValidator; namespace StardewModdingAPI.Web.Controllers @@ -21,11 +20,8 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Fields *********/ - /// <summary>The underlying Pastebin client.</summary> - private readonly IPastebinClient Pastebin; - - /// <summary>The underlying text compression helper.</summary> - private readonly IGzipHelper GzipHelper; + /// <summary>Provides access to raw data storage.</summary> + private readonly IStorageProvider Storage; /// <summary>The supported JSON schemas (names indexed by ID).</summary> private readonly IDictionary<string, string> SchemaFormats = new Dictionary<string, string> @@ -49,12 +45,10 @@ namespace StardewModdingAPI.Web.Controllers ** Constructor ***/ /// <summary>Construct an instance.</summary> - /// <param name="pastebin">The Pastebin API client.</param> - /// <param name="gzipHelper">The underlying text compression helper.</param> - public JsonValidatorController(IPastebinClient pastebin, IGzipHelper gzipHelper) + /// <param name="storage">Provides access to raw data storage.</param> + public JsonValidatorController(IStorageProvider storage) { - this.Pastebin = pastebin; - this.GzipHelper = gzipHelper; + this.Storage = storage; } /*** @@ -62,7 +56,7 @@ namespace StardewModdingAPI.Web.Controllers ***/ /// <summary>Render the schema validator UI.</summary> /// <param name="schemaName">The schema name with which to validate the JSON.</param> - /// <param name="id">The paste ID.</param> + /// <param name="id">The stored file ID.</param> [HttpGet] [Route("json")] [Route("json/{schemaName}")] @@ -76,16 +70,16 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", result); // fetch raw JSON - PasteInfo paste = await this.GetAsync(id); - if (string.IsNullOrWhiteSpace(paste.Content)) + StoredFileInfo file = await this.Storage.GetAsync(id); + if (string.IsNullOrWhiteSpace(file.Content)) return this.View("Index", result.SetUploadError("The JSON file seems to be empty.")); - result.SetContent(paste.Content); + result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning); // parse JSON JToken parsed; try { - parsed = JToken.Parse(paste.Content, new JsonLoadSettings + parsed = JToken.Parse(file.Content, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error, CommentHandling = CommentHandling.Load @@ -97,7 +91,7 @@ namespace StardewModdingAPI.Web.Controllers } // format JSON - result.SetContent(parsed.ToString(Formatting.Indented)); + result.SetContent(parsed.ToString(Formatting.Indented), expiry: file.Expiry, uploadWarning: file.Warning); // skip if no schema selected if (schemaName == "none") @@ -132,23 +126,20 @@ namespace StardewModdingAPI.Web.Controllers public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request) { if (request == null) - return this.View("Index", new JsonValidatorModel(null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid.")); + return this.View("Index", this.GetModel(null, null).SetUploadError("The request seems to be invalid.")); // normalize schema name string schemaName = this.NormalizeSchemaName(request.SchemaName); - // get raw log text + // get raw text string input = request.Content; if (string.IsNullOrWhiteSpace(input)) - return this.View("Index", new JsonValidatorModel(null, schemaName, this.SchemaFormats).SetUploadError("The JSON file seems to be empty.")); - - // upload log - input = this.GzipHelper.CompressString(input); - SavePasteResult result = await this.Pastebin.PostAsync($"JSON validator {DateTime.UtcNow:s}", input); + return this.View("Index", this.GetModel(null, schemaName).SetUploadError("The JSON file seems to be empty.")); - // handle errors - if (!result.Success) - return this.View("Index", new JsonValidatorModel(result.ID, schemaName, this.SchemaFormats).SetUploadError($"Pastebin error: {result.Error ?? "unknown error"}")); + // upload file + var result = await this.Storage.SaveAsync(title: $"JSON validator {DateTime.UtcNow:s}", content: input, compress: true); + if (!result.Succeeded) + return this.View("Index", this.GetModel(result.ID, schemaName).SetUploadError(result.UploadError)); // redirect to view return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID })); @@ -158,13 +149,12 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Private methods *********/ - /// <summary>Fetch raw text from Pastebin.</summary> - /// <param name="id">The Pastebin paste ID.</param> - private async Task<PasteInfo> GetAsync(string id) + /// <summary>Build a JSON validator model.</summary> + /// <param name="pasteID">The stored file ID.</param> + /// <param name="schemaName">The schema name with which the JSON was validated.</param> + private JsonValidatorModel GetModel(string pasteID, string schemaName) { - PasteInfo response = await this.Pastebin.GetAsync(id); - response.Content = this.GzipHelper.DecompressString(response.Content); - return response; + return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats); } /// <summary>Get a normalized schema name, or the <see cref="DefaultSchemaID"/> if blank.</summary> diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 318b34d0..e270ae0a 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,22 +1,12 @@ using System; -using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.S3; -using Amazon.S3.Model; -using Amazon.S3.Transfer; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Web.Framework; -using StardewModdingAPI.Web.Framework.Clients.Pastebin; -using StardewModdingAPI.Web.Framework.Compression; -using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.LogParsing; using StardewModdingAPI.Web.Framework.LogParsing.Models; +using StardewModdingAPI.Web.Framework.Storage; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers @@ -27,14 +17,8 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Fields *********/ - /// <summary>The API client settings.</summary> - private readonly ApiClientsConfig ClientsConfig; - - /// <summary>The underlying Pastebin client.</summary> - private readonly IPastebinClient Pastebin; - - /// <summary>The underlying text compression helper.</summary> - private readonly IGzipHelper GzipHelper; + /// <summary>Provides access to raw data storage.</summary> + private readonly IStorageProvider Storage; /********* @@ -44,21 +28,17 @@ namespace StardewModdingAPI.Web.Controllers ** Constructor ***/ /// <summary>Construct an instance.</summary> - /// <param name="clientsConfig">The API client settings.</param> - /// <param name="pastebin">The Pastebin API client.</param> - /// <param name="gzipHelper">The underlying text compression helper.</param> - public LogParserController(IOptions<ApiClientsConfig> clientsConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) + /// <param name="storage">Provides access to raw data storage.</param> + public LogParserController(IStorageProvider storage) { - this.ClientsConfig = clientsConfig.Value; - this.Pastebin = pastebin; - this.GzipHelper = gzipHelper; + this.Storage = storage; } /*** ** Web UI ***/ /// <summary>Render the log parser UI.</summary> - /// <param name="id">The paste ID.</param> + /// <param name="id">The stored file ID.</param> /// <param name="raw">Whether to display the raw unparsed log.</param> [HttpGet] [Route("log")] @@ -70,12 +50,12 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", this.GetModel(id)); // log page - PasteInfo paste = await this.GetAsync(id); - ParsedLog log = paste.Success - ? new LogParser().Parse(paste.Content) - : new ParsedLog { IsValid = false, Error = paste.Error }; + StoredFileInfo file = await this.Storage.GetAsync(id); + ParsedLog log = file.Success + ? new LogParser().Parse(file.Content) + : new ParsedLog { IsValid = false, Error = file.Error }; - return this.View("Index", this.GetModel(id, uploadWarning: paste.Warning, expiry: paste.Expiry).SetResult(log, raw)); + return this.View("Index", this.GetModel(id, uploadWarning: file.Warning, expiry: file.Expiry).SetResult(log, raw)); } /*** @@ -92,8 +72,7 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty.")); // upload log - input = this.GzipHelper.CompressString(input); - var uploadResult = await this.TrySaveLog(input); + UploadResult uploadResult = await this.Storage.SaveAsync(title: $"SMAPI log {DateTime.UtcNow:s}", content: input, compress: true); if (!uploadResult.Succeeded) return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError)); @@ -105,106 +84,8 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Private methods *********/ - /// <summary>Fetch raw text from Pastebin.</summary> - /// <param name="id">The Pastebin paste ID.</param> - private async Task<PasteInfo> GetAsync(string id) - { - // get from Amazon S3 - if (Guid.TryParseExact(id, "N", out Guid _)) - { - var credentials = new BasicAWSCredentials(accessKey: this.ClientsConfig.AmazonAccessKey, secretKey: this.ClientsConfig.AmazonSecretKey); - - using (IAmazonS3 s3 = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(this.ClientsConfig.AmazonRegion))) - { - try - { - using (GetObjectResponse response = await s3.GetObjectAsync(this.ClientsConfig.AmazonLogBucket, $"logs/{id}")) - using (Stream responseStream = response.ResponseStream) - using (StreamReader reader = new StreamReader(responseStream)) - { - DateTime expiry = response.Expiration.ExpiryDateUtc; - string pastebinError = response.Metadata["x-amz-meta-pastebin-error"]; - string content = this.GzipHelper.DecompressString(reader.ReadToEnd()); - - return new PasteInfo - { - Success = true, - Content = content, - Expiry = expiry, - Warning = pastebinError - }; - } - } - catch (AmazonServiceException ex) - { - return ex.ErrorCode == "NoSuchKey" - ? new PasteInfo { Error = "There's no log with that ID." } - : new PasteInfo { Error = $"Could not fetch that log from AWS S3 ({ex.ErrorCode}: {ex.Message})." }; - } - } - } - - // get from PasteBin - else - { - PasteInfo response = await this.Pastebin.GetAsync(id); - response.Content = this.GzipHelper.DecompressString(response.Content); - return response; - } - } - - /// <summary>Save a log to Pastebin or Amazon S3, if available.</summary> - /// <param name="content">The content to upload.</param> - /// <returns>Returns metadata about the save attempt.</returns> - private async Task<UploadResult> TrySaveLog(string content) - { - // save to PasteBin - string uploadError; - { - SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", content); - if (result.Success) - return new UploadResult(true, result.ID, null); - - uploadError = $"Pastebin error: {result.Error ?? "unknown error"}"; - } - - // fallback to S3 - try - { - var credentials = new BasicAWSCredentials(accessKey: this.ClientsConfig.AmazonAccessKey, secretKey: this.ClientsConfig.AmazonSecretKey); - - using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content))) - using (IAmazonS3 s3 = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(this.ClientsConfig.AmazonRegion))) - using (TransferUtility uploader = new TransferUtility(s3)) - { - string id = Guid.NewGuid().ToString("N"); - - var uploadRequest = new TransferUtilityUploadRequest - { - BucketName = this.ClientsConfig.AmazonLogBucket, - Key = $"logs/{id}", - InputStream = stream, - Metadata = - { - // note: AWS will lowercase keys and prefix 'x-amz-meta-' - ["smapi-uploaded"] = DateTime.UtcNow.ToString("O"), - ["pastebin-error"] = uploadError - } - }; - - await uploader.UploadAsync(uploadRequest); - - return new UploadResult(true, id, uploadError); - } - } - catch (Exception ex) - { - return new UploadResult(false, null, $"{uploadError}\n{ex.Message}"); - } - } - /// <summary>Build a log parser model.</summary> - /// <param name="pasteID">The paste ID.</param> + /// <param name="pasteID">The stored file ID.</param> /// <param name="expiry">When the uploaded file will no longer be available.</param> /// <param name="uploadWarning">A non-blocking warning while uploading the log.</param> /// <param name="uploadError">An error which occurred while uploading the log.</param> @@ -243,36 +124,5 @@ namespace StardewModdingAPI.Web.Controllers return null; } } - - /// <summary>The result of an attempt to upload a file.</summary> - private class UploadResult - { - /********* - ** Accessors - *********/ - /// <summary>Whether the file upload succeeded.</summary> - public bool Succeeded { get; } - - /// <summary>The file ID, if applicable.</summary> - public string ID { get; } - - /// <summary>The upload error, if any.</summary> - public string UploadError { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="succeeded">Whether the file upload succeeded.</param> - /// <param name="id">The file ID, if applicable.</param> - /// <param name="uploadError">The upload error, if any.</param> - public UploadResult(bool succeeded, string id, string uploadError) - { - this.Succeeded = succeeded; - this.ID = id; - this.UploadError = uploadError; - } - } } } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs index bb2de356..1ef3ef12 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs @@ -11,12 +11,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin /// <summary>The fetched paste content (if <see cref="Success"/> is <c>true</c>).</summary> public string Content { get; set; } - /// <summary>When the file will no longer be available.</summary> - public DateTime? Expiry { get; set; } - - /// <summary>The error message if saving succeeded, but a non-blocking issue was encountered.</summary> - public string Warning { get; set; } - /// <summary>The error message if saving failed.</summary> public string Error { get; set; } } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index 7119ef03..1e020840 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels public string AmazonRegion { get; set; } /// <summary>The AWS bucket in which to store temporary uploaded logs.</summary> - public string AmazonLogBucket { get; set; } + public string AmazonTempBucket { get; set; } /**** diff --git a/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs new file mode 100644 index 00000000..e222a235 --- /dev/null +++ b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace StardewModdingAPI.Web.Framework.Storage +{ + /// <summary>Provides access to raw data storage.</summary> + internal interface IStorageProvider + { + /// <summary>Save a text file to Pastebin or Amazon S3, if available.</summary> + /// <param name="title">The display title, if applicable.</param> + /// <param name="content">The content to upload.</param> + /// <param name="compress">Whether to gzip the text.</param> + /// <returns>Returns metadata about the save attempt.</returns> + Task<UploadResult> SaveAsync(string title, string content, bool compress = true); + + /// <summary>Fetch raw text from storage.</summary> + /// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param> + Task<StoredFileInfo> GetAsync(string id); + } +} diff --git a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs new file mode 100644 index 00000000..bbb6e06b --- /dev/null +++ b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Transfer; +using Microsoft.Extensions.Options; +using StardewModdingAPI.Web.Framework.Clients.Pastebin; +using StardewModdingAPI.Web.Framework.Compression; +using StardewModdingAPI.Web.Framework.ConfigModels; + +namespace StardewModdingAPI.Web.Framework.Storage +{ + /// <summary>Provides access to raw data storage.</summary> + internal class StorageProvider : IStorageProvider + { + /********* + ** Fields + *********/ + /// <summary>The API client settings.</summary> + private readonly ApiClientsConfig ClientsConfig; + + /// <summary>The underlying Pastebin client.</summary> + private readonly IPastebinClient Pastebin; + + /// <summary>The underlying text compression helper.</summary> + private readonly IGzipHelper GzipHelper; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="clientsConfig">The API client settings.</param> + /// <param name="pastebin">The underlying Pastebin client.</param> + /// <param name="gzipHelper">The underlying text compression helper.</param> + public StorageProvider(IOptions<ApiClientsConfig> clientsConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) + { + this.ClientsConfig = clientsConfig.Value; + this.Pastebin = pastebin; + this.GzipHelper = gzipHelper; + } + + /// <summary>Save a text file to Pastebin or Amazon S3, if available.</summary> + /// <param name="title">The display title, if applicable.</param> + /// <param name="content">The content to upload.</param> + /// <param name="compress">Whether to gzip the text.</param> + /// <returns>Returns metadata about the save attempt.</returns> + public async Task<UploadResult> SaveAsync(string title, string content, bool compress = true) + { + // save to PasteBin + string uploadError; + { + SavePasteResult result = await this.Pastebin.PostAsync(title, content); + if (result.Success) + return new UploadResult(true, result.ID, null); + + uploadError = $"Pastebin error: {result.Error ?? "unknown error"}"; + } + + // fallback to S3 + try + { + var credentials = new BasicAWSCredentials(accessKey: this.ClientsConfig.AmazonAccessKey, secretKey: this.ClientsConfig.AmazonSecretKey); + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + using IAmazonS3 s3 = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(this.ClientsConfig.AmazonRegion)); + using TransferUtility uploader = new TransferUtility(s3); + + string id = Guid.NewGuid().ToString("N"); + + var uploadRequest = new TransferUtilityUploadRequest + { + BucketName = this.ClientsConfig.AmazonTempBucket, + Key = $"uploads/{id}", + InputStream = stream, + Metadata = + { + // note: AWS will lowercase keys and prefix 'x-amz-meta-' + ["smapi-uploaded"] = DateTime.UtcNow.ToString("O"), + ["pastebin-error"] = uploadError + } + }; + + await uploader.UploadAsync(uploadRequest); + + return new UploadResult(true, id, uploadError); + } + catch (Exception ex) + { + return new UploadResult(false, null, $"{uploadError}\n{ex.Message}"); + } + } + + /// <summary>Fetch raw text from storage.</summary> + /// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param> + public async Task<StoredFileInfo> GetAsync(string id) + { + // get from Amazon S3 + if (Guid.TryParseExact(id, "N", out Guid _)) + { + var credentials = new BasicAWSCredentials(accessKey: this.ClientsConfig.AmazonAccessKey, secretKey: this.ClientsConfig.AmazonSecretKey); + using IAmazonS3 s3 = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(this.ClientsConfig.AmazonRegion)); + + try + { + using GetObjectResponse response = await s3.GetObjectAsync(this.ClientsConfig.AmazonTempBucket, $"uploads/{id}"); + using Stream responseStream = response.ResponseStream; + using StreamReader reader = new StreamReader(responseStream); + + DateTime expiry = response.Expiration.ExpiryDateUtc; + string pastebinError = response.Metadata["x-amz-meta-pastebin-error"]; + string content = this.GzipHelper.DecompressString(reader.ReadToEnd()); + + return new StoredFileInfo + { + Success = true, + Content = content, + Expiry = expiry, + Warning = pastebinError + }; + } + catch (AmazonServiceException ex) + { + return ex.ErrorCode == "NoSuchKey" + ? new StoredFileInfo { Error = "There's no file with that ID." } + : new StoredFileInfo { Error = $"Could not fetch that file from AWS S3 ({ex.ErrorCode}: {ex.Message})." }; + } + } + + // get from PasteBin + else + { + PasteInfo response = await this.Pastebin.GetAsync(id); + response.Content = this.GzipHelper.DecompressString(response.Content); + return new StoredFileInfo + { + Success = response.Success, + Content = response.Content, + Error = response.Error + }; + } + } + } +} diff --git a/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs b/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs new file mode 100644 index 00000000..30676c88 --- /dev/null +++ b/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs @@ -0,0 +1,23 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.Storage +{ + /// <summary>The response for a get-file request.</summary> + internal class StoredFileInfo + { + /// <summary>Whether the file was successfully fetched.</summary> + public bool Success { get; set; } + + /// <summary>The fetched file content (if <see cref="Success"/> is <c>true</c>).</summary> + public string Content { get; set; } + + /// <summary>When the file will no longer be available.</summary> + public DateTime? Expiry { get; set; } + + /// <summary>The error message if saving succeeded, but a non-blocking issue was encountered.</summary> + public string Warning { get; set; } + + /// <summary>The error message if saving failed.</summary> + public string Error { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Storage/UploadResult.cs b/src/SMAPI.Web/Framework/Storage/UploadResult.cs new file mode 100644 index 00000000..483c1769 --- /dev/null +++ b/src/SMAPI.Web/Framework/Storage/UploadResult.cs @@ -0,0 +1,33 @@ +namespace StardewModdingAPI.Web.Framework.Storage +{ + /// <summary>The result of an attempt to upload a file.</summary> + internal class UploadResult + { + /********* + ** Accessors + *********/ + /// <summary>Whether the file upload succeeded.</summary> + public bool Succeeded { get; } + + /// <summary>The file ID, if applicable.</summary> + public string ID { get; } + + /// <summary>The upload error, if any.</summary> + public string UploadError { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="succeeded">Whether the file upload succeeded.</param> + /// <param name="id">The file ID, if applicable.</param> + /// <param name="uploadError">The upload error, if any.</param> + public UploadResult(bool succeeded, string id, string uploadError) + { + this.Succeeded = succeeded; + this.ID = id; + this.UploadError = uploadError; + } + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 53823771..31b5e61d 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Rewrite; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using MongoDB.Bson.Serialization; using MongoDB.Driver; using Newtonsoft.Json; @@ -24,6 +25,7 @@ using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.Compression; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.RewriteRules; +using StardewModdingAPI.Web.Framework.Storage; namespace StardewModdingAPI.Web { @@ -158,7 +160,13 @@ namespace StardewModdingAPI.Web } // init helpers - services.AddSingleton<IGzipHelper>(new GzipHelper()); + services + .AddSingleton<IGzipHelper>(new GzipHelper()) + .AddSingleton<IStorageProvider>(serv => new StorageProvider( + serv.GetRequiredService<IOptions<ApiClientsConfig>>(), + serv.GetRequiredService<IPastebinClient>(), + serv.GetRequiredService<IGzipHelper>() + )); } /// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary> diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs index 5b18331f..c0dd7184 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -24,7 +25,13 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// <summary>The schema validation errors, if any.</summary> public JsonValidatorErrorModel[] Errors { get; set; } = new JsonValidatorErrorModel[0]; - /// <summary>An error which occurred while uploading the JSON to Pastebin.</summary> + /// <summary>A non-blocking warning while uploading the file.</summary> + public string UploadWarning { get; set; } + + /// <summary>When the uploaded file will no longer be available.</summary> + public DateTime? Expiry { get; set; } + + /// <summary>An error which occurred while uploading the JSON.</summary> public string UploadError { get; set; } /// <summary>An error which occurred while parsing the JSON.</summary> @@ -41,7 +48,7 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator public JsonValidatorModel() { } /// <summary>Construct an instance.</summary> - /// <param name="pasteID">The paste ID.</param> + /// <param name="pasteID">The stored file ID.</param> /// <param name="schemaName">The schema name with which the JSON was validated.</param> /// <param name="schemaFormats">The supported JSON schemas (names indexed by ID).</param> public JsonValidatorModel(string pasteID, string schemaName, IDictionary<string, string> schemaFormats) @@ -53,14 +60,18 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// <summary>Set the validated content.</summary> /// <param name="content">The validated content.</param> - public JsonValidatorModel SetContent(string content) + /// <param name="expiry">When the uploaded file will no longer be available.</param> + /// <param name="uploadWarning">A non-blocking warning while uploading the log.</param> + public JsonValidatorModel SetContent(string content, DateTime? expiry, string uploadWarning = null) { this.Content = content; + this.Expiry = expiry; + this.UploadWarning = uploadWarning; return this; } - /// <summary>Set the error which occurred while uploading the log to Pastebin.</summary> + /// <summary>Set the error which occurred while uploading the JSON.</summary> /// <param name="error">The error message.</param> public JsonValidatorModel SetUploadError(string error) { diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index de6b06a2..a5a134ac 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -1,3 +1,4 @@ +@using Humanizer @using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.ViewModels.JsonValidator @model JsonValidatorModel @@ -26,7 +27,7 @@ { <meta name="robots" content="noindex" /> } - <link rel="stylesheet" href="~/Content/css/json-validator.css" /> + <link rel="stylesheet" href="~/Content/css/json-validator.css?r=20191203" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/themes/sunlight.default.min.css" /> <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script> @@ -67,6 +68,18 @@ else if (Model.PasteID != null) </div> } +@* save warnings *@ +@if (Model.UploadWarning != null || Model.Expiry != null) +{ + <div class="save-metadata" v-pre> + @if (Model.Expiry != null) + { + <text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()). </text> + } + <!--@Model.UploadWarning--> + </div> +} + @* upload new file *@ @if (Model.Content == null) { diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index f81587ef..b3567469 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -26,7 +26,7 @@ "AmazonAccessKey": null, "AmazonSecretKey": null, "AmazonRegion": "us-east-1", - "AmazonLogBucket": "smapi-log-parser", + "AmazonTempBucket": "smapi-web-temp", "ChucklefishBaseUrl": "https://community.playstarbound.com", "ChucklefishModPageUrlFormat": "resources/{0}", diff --git a/src/SMAPI.Web/wwwroot/Content/css/json-validator.css b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css index cd117694..18195098 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/json-validator.css +++ b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css @@ -41,6 +41,12 @@ background: #FCC; } +.save-metadata { + margin-top: 1em; + font-size: 0.8em; + opacity: 0.3; +} + /********* ** Validation results *********/ |