allow local dev environments without an Azure account
This commit is contained in:
parent
ba46491ebc
commit
c1b15fb372
|
@ -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.
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,17 +49,19 @@ 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)
|
||||
{
|
||||
string id = Guid.NewGuid().ToString("N");
|
||||
|
||||
// save to Azure
|
||||
if (this.HasAzure)
|
||||
{
|
||||
try
|
||||
{
|
||||
using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
string id = Guid.NewGuid().ToString("N");
|
||||
|
||||
BlobClient blob = this.GetAzureBlobClient(id);
|
||||
await blob.UploadAsync(stream);
|
||||
|
||||
|
@ -65,14 +73,27 @@ namespace StardewModdingAPI.Web.Framework.Storage
|
|||
}
|
||||
}
|
||||
|
||||
// save to local filesystem for testing
|
||||
else
|
||||
{
|
||||
string path = this.GetDevFilePath(id);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
File.WriteAllText(path, content);
|
||||
return new UploadResult(true, id, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// fetch from Azure/Amazon
|
||||
// fetch from blob storage
|
||||
if (Guid.TryParseExact(id, "N", out Guid _))
|
||||
{
|
||||
// try Azure
|
||||
// Azure Blob storage
|
||||
if (this.HasAzure)
|
||||
{
|
||||
try
|
||||
{
|
||||
BlobClient blob = this.GetAzureBlobClient(id);
|
||||
|
@ -80,7 +101,7 @@ namespace StardewModdingAPI.Web.Framework.Storage
|
|||
using BlobDownloadInfo result = response.Value;
|
||||
|
||||
using StreamReader reader = new StreamReader(result.Content);
|
||||
DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ClientsConfig.AzureBlobTempExpiryDays);
|
||||
DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ExpiryDays);
|
||||
string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
|
||||
|
||||
return new StoredFileInfo
|
||||
|
@ -101,7 +122,33 @@ namespace StardewModdingAPI.Web.Framework.Storage
|
|||
}
|
||||
}
|
||||
|
||||
// get from PasteBin
|
||||
// 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 = "There's no file with that ID."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue