summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md3
-rw-r--r--src/SMAPI.Web/Controllers/JsonValidatorController.cs2
-rw-r--r--src/SMAPI.Web/Controllers/LogParserController.cs2
-rw-r--r--src/SMAPI.Web/Framework/Storage/IStorageProvider.cs3
-rw-r--r--src/SMAPI.Web/Framework/Storage/StorageProvider.cs116
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml21
6 files changed, 104 insertions, 43 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 591f4cc2..bbe08c13 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -26,6 +26,9 @@
* Fixed private textures loaded from content packs not having their `Name` field set.
* Fixed asset propagation for `Characters\Farmer\farmer_girl_base_bald`.
+* For SMAPI developers:
+ * You can now run local environments without configuring Amazon, Azure, and Pastebin accounts.
+
## 3.0.1
Released 02 December 2019 for Stardew Valley 1.4.0.1.
diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs
index c4bfff3b..2ade3e3d 100644
--- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs
+++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs
@@ -141,7 +141,7 @@ namespace StardewModdingAPI.Web.Controllers
return this.View("Index", this.GetModel(null, schemaName).SetUploadError("The JSON file seems to be empty."));
// upload file
- UploadResult result = await this.Storage.SaveAsync(title: $"JSON validator {DateTime.UtcNow:s}", content: input, compress: true);
+ UploadResult result = await this.Storage.SaveAsync(input);
if (!result.Succeeded)
return this.View("Index", this.GetModel(result.ID, schemaName).SetUploadError(result.UploadError));
diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs
index e270ae0a..97c419d9 100644
--- a/src/SMAPI.Web/Controllers/LogParserController.cs
+++ b/src/SMAPI.Web/Controllers/LogParserController.cs
@@ -72,7 +72,7 @@ namespace StardewModdingAPI.Web.Controllers
return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty."));
// upload log
- UploadResult uploadResult = await this.Storage.SaveAsync(title: $"SMAPI log {DateTime.UtcNow:s}", content: input, compress: true);
+ UploadResult uploadResult = await this.Storage.SaveAsync(input);
if (!uploadResult.Succeeded)
return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError));
diff --git a/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
index 12a5e421..96a34fbb 100644
--- a/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
+++ b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
@@ -6,11 +6,10 @@ namespace StardewModdingAPI.Web.Framework.Storage
internal interface IStorageProvider
{
/// <summary>Save a text file to storage.</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);
+ Task<UploadResult> SaveAsync(string content, bool compress = true);
/// <summary>Fetch raw text from storage.</summary>
/// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param>
diff --git a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
index 12a35f18..35538443 100644
--- a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
+++ b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
@@ -27,6 +27,12 @@ namespace StardewModdingAPI.Web.Framework.Storage
/// <summary>The underlying text compression helper.</summary>
private readonly IGzipHelper GzipHelper;
+ /// <summary>Whether Azure blob storage is configured.</summary>
+ private bool HasAzure => !string.IsNullOrWhiteSpace(this.ClientsConfig.AzureBlobConnectionString);
+
+ /// <summary>The number of days since the blob's last-modified date when it will be deleted.</summary>
+ private int ExpiryDays => this.ClientsConfig.AzureBlobTempExpiryDays;
+
/*********
** Public methods
@@ -43,25 +49,38 @@ namespace StardewModdingAPI.Web.Framework.Storage
}
/// <summary>Save a text file to storage.</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)
+ public async Task<UploadResult> SaveAsync(string content, bool compress = true)
{
- try
- {
- using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
- string id = Guid.NewGuid().ToString("N");
+ string id = Guid.NewGuid().ToString("N");
- BlobClient blob = this.GetAzureBlobClient(id);
- await blob.UploadAsync(stream);
+ // save to Azure
+ if (this.HasAzure)
+ {
+ try
+ {
+ using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
+ BlobClient blob = this.GetAzureBlobClient(id);
+ await blob.UploadAsync(stream);
- return new UploadResult(true, id, null);
+ return new UploadResult(true, id, null);
+ }
+ catch (Exception ex)
+ {
+ return new UploadResult(false, null, ex.Message);
+ }
}
- catch (Exception ex)
+
+ // save to local filesystem for testing
+ else
{
- return new UploadResult(false, null, ex.Message);
+ string path = this.GetDevFilePath(id);
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+ File.WriteAllText(path, content);
+ return new UploadResult(true, id, null);
}
}
@@ -69,39 +88,67 @@ namespace StardewModdingAPI.Web.Framework.Storage
/// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param>
public async Task<StoredFileInfo> GetAsync(string id)
{
- // fetch from Azure/Amazon
+ // fetch from blob storage
if (Guid.TryParseExact(id, "N", out Guid _))
{
- // try Azure
- try
+ // Azure Blob storage
+ if (this.HasAzure)
{
- BlobClient blob = this.GetAzureBlobClient(id);
- Response<BlobDownloadInfo> response = await blob.DownloadAsync();
- using BlobDownloadInfo result = response.Value;
+ try
+ {
+ BlobClient blob = this.GetAzureBlobClient(id);
+ Response<BlobDownloadInfo> response = await blob.DownloadAsync();
+ using BlobDownloadInfo result = response.Value;
- using StreamReader reader = new StreamReader(result.Content);
- DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ClientsConfig.AzureBlobTempExpiryDays);
- string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
+ using StreamReader reader = new StreamReader(result.Content);
+ DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ExpiryDays);
+ string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
- return new StoredFileInfo
+ return new StoredFileInfo
+ {
+ Success = true,
+ Content = content,
+ Expiry = expiry.UtcDateTime
+ };
+ }
+ catch (RequestFailedException ex)
{
- Success = true,
- Content = content,
- Expiry = expiry.UtcDateTime
- };
+ return new StoredFileInfo
+ {
+ Error = ex.ErrorCode == "BlobNotFound"
+ ? "There's no file with that ID."
+ : $"Could not fetch that file from storage ({ex.ErrorCode}: {ex.Message})."
+ };
+ }
}
- catch (RequestFailedException ex)
+
+ // local filesystem for testing
+ else
{
+ FileInfo file = new FileInfo(this.GetDevFilePath(id));
+ if (file.Exists)
+ {
+ if (file.LastWriteTimeUtc.AddDays(this.ExpiryDays) < DateTime.UtcNow)
+ file.Delete();
+ else
+ {
+ return new StoredFileInfo
+ {
+ Success = true,
+ Content = File.ReadAllText(file.FullName),
+ Expiry = DateTime.UtcNow.AddDays(this.ExpiryDays),
+ Warning = "This file was saved temporarily to the local computer. This should only happen in a local development environment."
+ };
+ }
+ }
return new StoredFileInfo
{
- Error = ex.ErrorCode == "BlobNotFound"
- ? "There's no file with that ID."
- : $"Could not fetch that file from storage ({ex.ErrorCode}: {ex.Message})."
+ Error = "There's no file with that ID."
};
}
}
- // get from PasteBin
+ // get from Pastebin
else
{
PasteInfo response = await this.Pastebin.GetAsync(id);
@@ -116,12 +163,19 @@ namespace StardewModdingAPI.Web.Framework.Storage
}
/// <summary>Get a client for reading and writing to Azure Blob storage.</summary>
- /// <param name="id">The file ID to fetch.</param>
+ /// <param name="id">The file ID.</param>
private BlobClient GetAzureBlobClient(string id)
{
var azure = new BlobServiceClient(this.ClientsConfig.AzureBlobConnectionString);
var container = azure.GetBlobContainerClient(this.ClientsConfig.AzureBlobTempContainer);
return container.GetBlobClient($"uploads/{id}");
}
+
+ /// <summary>Get the absolute file path for an upload when running in a local test environment with no Azure account configured.</summary>
+ /// <param name="id">The file ID.</param>
+ private string GetDevFilePath(string id)
+ {
+ return Path.Combine(Path.GetTempPath(), "smapi-web-temp", $"{id}.txt");
+ }
}
}
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 439167bc..ac951564 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -67,12 +67,16 @@ else if (Model.ParsedLog?.IsValid == true)
@* save warnings *@
@if (Model.UploadWarning != null || Model.Expiry != null)
{
+ @if (Model.UploadWarning != null)
+ {
+ <text>⚠️ @Model.UploadWarning<br /></text>
+ }
+
<div class="save-metadata" v-pre>
@if (Model.Expiry != null)
{
- <text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()). </text>
+ <text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()).</text>
}
- <!--@Model.UploadWarning-->
</div>
}
@@ -294,10 +298,7 @@ else if (Model.ParsedLog?.IsValid == true)
string sectionFilter = message.Section != null && !message.IsStartOfSection ? $"&& sectionsAllow('{message.Section}')" : null; // filter the message by section if applicable
<tr class="mod @levelStr @sectionStartClass"
- @if (message.IsStartOfSection)
- {
- <text>v-on:click="toggleSection('@message.Section')"</text>
- }
+ @if (message.IsStartOfSection) { <text> v-on:click="toggleSection('@message.Section')" </text> }
v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter">
<td v-pre>@message.Time</td>
<td v-pre>@message.Level.ToString().ToUpper()</td>
@@ -307,8 +308,12 @@ else if (Model.ParsedLog?.IsValid == true)
@if (message.IsStartOfSection)
{
<span class="section-toggle-message">
- <template v-if="sectionsAllow('@message.Section')">This section is shown. Click here to hide it.</template>
- <template v-else>This section is hidden. Click here to show it.</template>
+ <template v-if="sectionsAllow('@message.Section')">
+ This section is shown. Click here to hide it.
+ </template>
+ <template v-else>
+ This section is hidden. Click here to show it.
+ </template>
</span>
}
</td>