Skip to content

Commit c1b15fb

Browse files
committed
allow local dev environments without an Azure account
1 parent ba46491 commit c1b15fb

6 files changed

Lines changed: 104 additions & 43 deletions

File tree

docs/release-notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
* Fixed private textures loaded from content packs not having their `Name` field set.
2727
* Fixed asset propagation for `Characters\Farmer\farmer_girl_base_bald`.
2828

29+
* For SMAPI developers:
30+
* You can now run local environments without configuring Amazon, Azure, and Pastebin accounts.
31+
2932
## 3.0.1
3033
Released 02 December 2019 for Stardew Valley 1.4.0.1.
3134

src/SMAPI.Web/Controllers/JsonValidatorController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request)
141141
return this.View("Index", this.GetModel(null, schemaName).SetUploadError("The JSON file seems to be empty."));
142142

143143
// upload file
144-
UploadResult result = await this.Storage.SaveAsync(title: $"JSON validator {DateTime.UtcNow:s}", content: input, compress: true);
144+
UploadResult result = await this.Storage.SaveAsync(input);
145145
if (!result.Succeeded)
146146
return this.View("Index", this.GetModel(result.ID, schemaName).SetUploadError(result.UploadError));
147147

src/SMAPI.Web/Controllers/LogParserController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public async Task<ActionResult> PostAsync()
7272
return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty."));
7373

7474
// upload log
75-
UploadResult uploadResult = await this.Storage.SaveAsync(title: $"SMAPI log {DateTime.UtcNow:s}", content: input, compress: true);
75+
UploadResult uploadResult = await this.Storage.SaveAsync(input);
7676
if (!uploadResult.Succeeded)
7777
return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError));
7878

src/SMAPI.Web/Framework/Storage/IStorageProvider.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ namespace StardewModdingAPI.Web.Framework.Storage
66
internal interface IStorageProvider
77
{
88
/// <summary>Save a text file to storage.</summary>
9-
/// <param name="title">The display title, if applicable.</param>
109
/// <param name="content">The content to upload.</param>
1110
/// <param name="compress">Whether to gzip the text.</param>
1211
/// <returns>Returns metadata about the save attempt.</returns>
13-
Task<UploadResult> SaveAsync(string title, string content, bool compress = true);
12+
Task<UploadResult> SaveAsync(string content, bool compress = true);
1413

1514
/// <summary>Fetch raw text from storage.</summary>
1615
/// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param>

src/SMAPI.Web/Framework/Storage/StorageProvider.cs

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ internal class StorageProvider : IStorageProvider
2727
/// <summary>The underlying text compression helper.</summary>
2828
private readonly IGzipHelper GzipHelper;
2929

30+
/// <summary>Whether Azure blob storage is configured.</summary>
31+
private bool HasAzure => !string.IsNullOrWhiteSpace(this.ClientsConfig.AzureBlobConnectionString);
32+
33+
/// <summary>The number of days since the blob's last-modified date when it will be deleted.</summary>
34+
private int ExpiryDays => this.ClientsConfig.AzureBlobTempExpiryDays;
35+
3036

3137
/*********
3238
** Public methods
@@ -43,65 +49,106 @@ public StorageProvider(IOptions<ApiClientsConfig> clientsConfig, IPastebinClient
4349
}
4450

4551
/// <summary>Save a text file to storage.</summary>
46-
/// <param name="title">The display title, if applicable.</param>
4752
/// <param name="content">The content to upload.</param>
4853
/// <param name="compress">Whether to gzip the text.</param>
4954
/// <returns>Returns metadata about the save attempt.</returns>
50-
public async Task<UploadResult> SaveAsync(string title, string content, bool compress = true)
55+
public async Task<UploadResult> SaveAsync(string content, bool compress = true)
5156
{
52-
try
53-
{
54-
using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
55-
string id = Guid.NewGuid().ToString("N");
57+
string id = Guid.NewGuid().ToString("N");
5658

57-
BlobClient blob = this.GetAzureBlobClient(id);
58-
await blob.UploadAsync(stream);
59+
// save to Azure
60+
if (this.HasAzure)
61+
{
62+
try
63+
{
64+
using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
65+
BlobClient blob = this.GetAzureBlobClient(id);
66+
await blob.UploadAsync(stream);
5967

60-
return new UploadResult(true, id, null);
68+
return new UploadResult(true, id, null);
69+
}
70+
catch (Exception ex)
71+
{
72+
return new UploadResult(false, null, ex.Message);
73+
}
6174
}
62-
catch (Exception ex)
75+
76+
// save to local filesystem for testing
77+
else
6378
{
64-
return new UploadResult(false, null, ex.Message);
79+
string path = this.GetDevFilePath(id);
80+
Directory.CreateDirectory(Path.GetDirectoryName(path));
81+
82+
File.WriteAllText(path, content);
83+
return new UploadResult(true, id, null);
6584
}
6685
}
6786

6887
/// <summary>Fetch raw text from storage.</summary>
6988
/// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param>
7089
public async Task<StoredFileInfo> GetAsync(string id)
7190
{
72-
// fetch from Azure/Amazon
91+
// fetch from blob storage
7392
if (Guid.TryParseExact(id, "N", out Guid _))
7493
{
75-
// try Azure
76-
try
94+
// Azure Blob storage
95+
if (this.HasAzure)
7796
{
78-
BlobClient blob = this.GetAzureBlobClient(id);
79-
Response<BlobDownloadInfo> response = await blob.DownloadAsync();
80-
using BlobDownloadInfo result = response.Value;
97+
try
98+
{
99+
BlobClient blob = this.GetAzureBlobClient(id);
100+
Response<BlobDownloadInfo> response = await blob.DownloadAsync();
101+
using BlobDownloadInfo result = response.Value;
81102

82-
using StreamReader reader = new StreamReader(result.Content);
83-
DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ClientsConfig.AzureBlobTempExpiryDays);
84-
string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
103+
using StreamReader reader = new StreamReader(result.Content);
104+
DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ExpiryDays);
105+
string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
85106

86-
return new StoredFileInfo
107+
return new StoredFileInfo
108+
{
109+
Success = true,
110+
Content = content,
111+
Expiry = expiry.UtcDateTime
112+
};
113+
}
114+
catch (RequestFailedException ex)
87115
{
88-
Success = true,
89-
Content = content,
90-
Expiry = expiry.UtcDateTime
91-
};
116+
return new StoredFileInfo
117+
{
118+
Error = ex.ErrorCode == "BlobNotFound"
119+
? "There's no file with that ID."
120+
: $"Could not fetch that file from storage ({ex.ErrorCode}: {ex.Message})."
121+
};
122+
}
92123
}
93-
catch (RequestFailedException ex)
124+
125+
// local filesystem for testing
126+
else
94127
{
128+
FileInfo file = new FileInfo(this.GetDevFilePath(id));
129+
if (file.Exists)
130+
{
131+
if (file.LastWriteTimeUtc.AddDays(this.ExpiryDays) < DateTime.UtcNow)
132+
file.Delete();
133+
else
134+
{
135+
return new StoredFileInfo
136+
{
137+
Success = true,
138+
Content = File.ReadAllText(file.FullName),
139+
Expiry = DateTime.UtcNow.AddDays(this.ExpiryDays),
140+
Warning = "This file was saved temporarily to the local computer. This should only happen in a local development environment."
141+
};
142+
}
143+
}
95144
return new StoredFileInfo
96145
{
97-
Error = ex.ErrorCode == "BlobNotFound"
98-
? "There's no file with that ID."
99-
: $"Could not fetch that file from storage ({ex.ErrorCode}: {ex.Message})."
146+
Error = "There's no file with that ID."
100147
};
101148
}
102149
}
103150

104-
// get from PasteBin
151+
// get from Pastebin
105152
else
106153
{
107154
PasteInfo response = await this.Pastebin.GetAsync(id);
@@ -116,12 +163,19 @@ public async Task<StoredFileInfo> GetAsync(string id)
116163
}
117164

118165
/// <summary>Get a client for reading and writing to Azure Blob storage.</summary>
119-
/// <param name="id">The file ID to fetch.</param>
166+
/// <param name="id">The file ID.</param>
120167
private BlobClient GetAzureBlobClient(string id)
121168
{
122169
var azure = new BlobServiceClient(this.ClientsConfig.AzureBlobConnectionString);
123170
var container = azure.GetBlobContainerClient(this.ClientsConfig.AzureBlobTempContainer);
124171
return container.GetBlobClient($"uploads/{id}");
125172
}
173+
174+
/// <summary>Get the absolute file path for an upload when running in a local test environment with no Azure account configured.</summary>
175+
/// <param name="id">The file ID.</param>
176+
private string GetDevFilePath(string id)
177+
{
178+
return Path.Combine(Path.GetTempPath(), "smapi-web-temp", $"{id}.txt");
179+
}
126180
}
127181
}

src/SMAPI.Web/Views/LogParser/Index.cshtml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,16 @@ else if (Model.ParsedLog?.IsValid == true)
6767
@* save warnings *@
6868
@if (Model.UploadWarning != null || Model.Expiry != null)
6969
{
70+
@if (Model.UploadWarning != null)
71+
{
72+
<text>⚠️ @Model.UploadWarning<br /></text>
73+
}
74+
7075
<div class="save-metadata" v-pre>
7176
@if (Model.Expiry != null)
7277
{
73-
<text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()). </text>
78+
<text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()).</text>
7479
}
75-
7680
</div>
7781
}
7882
@@ -294,10 +298,7 @@ else if (Model.ParsedLog?.IsValid == true)
294298
string sectionFilter = message.Section != null && !message.IsStartOfSection ? $"&& sectionsAllow('{message.Section}')" : null; // filter the message by section if applicable
295299
296300
<tr class="mod @levelStr @sectionStartClass"
297-
@if (message.IsStartOfSection)
298-
{
299-
<text>v-on:click="toggleSection('@message.Section')"</text>
300-
}
301+
@if (message.IsStartOfSection) { <text> v-on:click="toggleSection('@message.Section')" </text> }
301302
v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter">
302303
<td v-pre>@message.Time</td>
303304
<td v-pre>@message.Level.ToString().ToUpper()</td>
@@ -307,8 +308,12 @@ else if (Model.ParsedLog?.IsValid == true)
307308
@if (message.IsStartOfSection)
308309
{
309310
<span class="section-toggle-message">
310-
<template v-if="sectionsAllow('@message.Section')">This section is shown. Click here to hide it.</template>
311-
<template v-else>This section is hidden. Click here to show it.</template>
311+
<template v-if="sectionsAllow('@message.Section')">
312+
This section is shown. Click here to hide it.
313+
</template>
314+
<template v-else>
315+
This section is hidden. Click here to show it.
316+
</template>
312317
</span>
313318
}
314319
</td>

0 commit comments

Comments
 (0)