summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Controllers/LogParserController.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.Web/Controllers/LogParserController.cs')
-rw-r--r--src/SMAPI.Web/Controllers/LogParserController.cs175
1 files changed, 153 insertions, 22 deletions
diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs
index f7f19cd8..318b34d0 100644
--- a/src/SMAPI.Web/Controllers/LogParserController.cs
+++ b/src/SMAPI.Web/Controllers/LogParserController.cs
@@ -1,6 +1,13 @@
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;
@@ -20,8 +27,8 @@ namespace StardewModdingAPI.Web.Controllers
/*********
** Fields
*********/
- /// <summary>The site config settings.</summary>
- private readonly SiteConfig Config;
+ /// <summary>The API client settings.</summary>
+ private readonly ApiClientsConfig ClientsConfig;
/// <summary>The underlying Pastebin client.</summary>
private readonly IPastebinClient Pastebin;
@@ -37,12 +44,12 @@ namespace StardewModdingAPI.Web.Controllers
** Constructor
***/
/// <summary>Construct an instance.</summary>
- /// <param name="siteConfig">The context config settings.</param>
+ /// <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<SiteConfig> siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper)
+ public LogParserController(IOptions<ApiClientsConfig> clientsConfig, IPastebinClient pastebin, IGzipHelper gzipHelper)
{
- this.Config = siteConfig.Value;
+ this.ClientsConfig = clientsConfig.Value;
this.Pastebin = pastebin;
this.GzipHelper = gzipHelper;
}
@@ -66,8 +73,9 @@ namespace StardewModdingAPI.Web.Controllers
PasteInfo paste = await this.GetAsync(id);
ParsedLog log = paste.Success
? new LogParser().Parse(paste.Content)
- : new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error };
- return this.View("Index", this.GetModel(id).SetResult(log, raw));
+ : new ParsedLog { IsValid = false, Error = paste.Error };
+
+ return this.View("Index", this.GetModel(id, uploadWarning: paste.Warning, expiry: paste.Expiry).SetResult(log, raw));
}
/***
@@ -85,16 +93,12 @@ namespace StardewModdingAPI.Web.Controllers
// upload log
input = this.GzipHelper.CompressString(input);
- SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", input);
-
- // handle errors
- if (!result.Success)
- return this.View("Index", this.GetModel(result.ID, uploadError: $"Pastebin error: {result.Error ?? "unknown error"}"));
+ var uploadResult = await this.TrySaveLog(input);
+ if (!uploadResult.Succeeded)
+ return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError));
// redirect to view
- UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl));
- uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID;
- return this.Redirect(uri.Uri.ToString());
+ return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID }));
}
@@ -105,19 +109,115 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="id">The Pastebin paste ID.</param>
private async Task<PasteInfo> GetAsync(string id)
{
- PasteInfo response = await this.Pastebin.GetAsync(id);
- response.Content = this.GzipHelper.DecompressString(response.Content);
- return response;
+ // 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="uploadError">An error which occurred while uploading the log to Pastebin.</param>
- private LogParserModel GetModel(string pasteID, string uploadError = null)
+ /// <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>
+ private LogParserModel GetModel(string pasteID, DateTime? expiry = null, string uploadWarning = null, string uploadError = null)
{
- string sectionUrl = this.Config.LogParserUrl;
Platform? platform = this.DetectClientPlatform();
- return new LogParserModel(sectionUrl, pasteID, platform) { UploadError = uploadError };
+
+ return new LogParserModel(pasteID, platform)
+ {
+ UploadWarning = uploadWarning,
+ UploadError = uploadError,
+ Expiry = expiry
+ };
}
/// <summary>Detect the viewer's OS.</summary>
@@ -143,5 +243,36 @@ 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;
+ }
+ }
}
}