summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2019-12-03 21:21:28 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2019-12-03 21:21:28 -0500
commit2b1f607d41b3d4d071c0db0671dbc99b6982909f (patch)
treea854b7465c73a38c12b3b841515e39d9154217a7 /src/SMAPI.Web
parent0aad3f545af854619c641dc8b57ac0cf12971c8e (diff)
downloadSMAPI-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.cs58
-rw-r--r--src/SMAPI.Web/Controllers/LogParserController.cs178
-rw-r--r--src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs6
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs2
-rw-r--r--src/SMAPI.Web/Framework/Storage/IStorageProvider.cs19
-rw-r--r--src/SMAPI.Web/Framework/Storage/StorageProvider.cs147
-rw-r--r--src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs23
-rw-r--r--src/SMAPI.Web/Framework/Storage/UploadResult.cs33
-rw-r--r--src/SMAPI.Web/Startup.cs10
-rw-r--r--src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs19
-rw-r--r--src/SMAPI.Web/Views/JsonValidator/Index.cshtml15
-rw-r--r--src/SMAPI.Web/appsettings.json2
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/json-validator.css6
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
*********/