summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs5
-rw-r--r--src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs58
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs26
-rw-r--r--src/SMAPI.Web/Framework/Storage/IStorageProvider.cs2
-rw-r--r--src/SMAPI.Web/Framework/Storage/StorageProvider.cs108
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj1
-rw-r--r--src/SMAPI.Web/Startup.cs4
-rw-r--r--src/SMAPI.Web/appsettings.Development.json7
-rw-r--r--src/SMAPI.Web/appsettings.json9
9 files changed, 87 insertions, 133 deletions
diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs
index a635abe3..431fed7b 100644
--- a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs
@@ -9,10 +9,5 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
/// <summary>Fetch a saved paste.</summary>
/// <param name="id">The paste ID.</param>
Task<PasteInfo> GetAsync(string id);
-
- /// <summary>Save a paste to Pastebin.</summary>
- /// <param name="name">The paste name.</param>
- /// <param name="content">The paste content.</param>
- Task<SavePasteResult> PostAsync(string name, string content);
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
index d695aab6..1be00be7 100644
--- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
@@ -1,7 +1,5 @@
using System;
-using System.Linq;
using System.Net;
-using System.Net.Http;
using System.Threading.Tasks;
using Pathoschild.Http.Client;
@@ -16,12 +14,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
- /// <summary>The user key used to authenticate with the Pastebin API.</summary>
- private readonly string UserKey;
-
- /// <summary>The developer key used to authenticate with the Pastebin API.</summary>
- private readonly string DevKey;
-
/*********
** Public methods
@@ -29,13 +21,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
/// <summary>Construct an instance.</summary>
/// <param name="baseUrl">The base URL for the Pastebin API.</param>
/// <param name="userAgent">The user agent for the API client.</param>
- /// <param name="userKey">The user key used to authenticate with the Pastebin API.</param>
- /// <param name="devKey">The developer key used to authenticate with the Pastebin API.</param>
- public PastebinClient(string baseUrl, string userAgent, string userKey, string devKey)
+ public PastebinClient(string baseUrl, string userAgent)
{
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
- this.UserKey = userKey;
- this.DevKey = devKey;
}
/// <summary>Fetch a saved paste.</summary>
@@ -66,50 +54,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
}
}
- /// <summary>Save a paste to Pastebin.</summary>
- /// <param name="name">The paste name.</param>
- /// <param name="content">The paste content.</param>
- public async Task<SavePasteResult> PostAsync(string name, string content)
- {
- try
- {
- // validate
- if (string.IsNullOrWhiteSpace(content))
- return new SavePasteResult { Error = "The log content can't be empty." };
-
- // post to API
- string response = await this.Client
- .PostAsync("api/api_post.php")
- .WithBody(p => p.FormUrlEncoded(new
- {
- api_option = "paste",
- api_user_key = this.UserKey,
- api_dev_key = this.DevKey,
- api_paste_private = 1, // unlisted
- api_paste_name = name,
- api_paste_expire_date = "N", // never expire
- api_paste_code = content
- }))
- .AsString();
-
- // handle Pastebin errors
- if (string.IsNullOrWhiteSpace(response))
- return new SavePasteResult { Error = "Received an empty response from Pastebin." };
- if (response.StartsWith("Bad API request"))
- return new SavePasteResult { Error = response };
- if (!response.Contains("/"))
- return new SavePasteResult { Error = $"Received an unknown response: {response}" };
-
- // return paste ID
- string pastebinID = response.Split("/").Last();
- return new SavePasteResult { Success = true, ID = pastebinID };
- }
- catch (Exception ex)
- {
- return new SavePasteResult { Success = false, Error = ex.ToString() };
- }
- }
-
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
index e88f351f..4a73750b 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
@@ -30,6 +30,19 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/****
+ ** Azure
+ ****/
+ /// <summary>The connection string for the Azure Blob storage account.</summary>
+ public string AzureBlobConnectionString { get; set; }
+
+ /// <summary>The Azure Blob container in which to store temporary uploaded logs.</summary>
+ public string AzureBlobTempContainer { get; set; }
+
+ /// <summary>The number of days since the blob's last-modified date when it will be deleted.</summary>
+ public int AzureBlobTempExpiryDays { get; set; }
+
+
+ /****
** Chucklefish
****/
/// <summary>The base URL for the Chucklefish mod site.</summary>
@@ -61,6 +74,7 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
public string GitHubPassword { get; set; }
+
/****
** ModDrop
****/
@@ -70,6 +84,7 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The URL for a ModDrop mod page for the user, where {0} is the mod ID.</summary>
public string ModDropModPageUrl { get; set; }
+
/****
** Nexus Mods
****/
@@ -85,20 +100,11 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The Nexus API authentication key.</summary>
public string NexusApiKey { get; set; }
+
/****
** Pastebin
****/
/// <summary>The base URL for the Pastebin API.</summary>
public string PastebinBaseUrl { get; set; }
-
- /// <summary>The user key used to authenticate with the Pastebin API.</summary>
- public string PastebinUserKey { get; set; }
-
- /// <summary>The developer key used to authenticate with the Pastebin API.</summary>
- public string PastebinDevKey { get; set; }
-
- /// <summary>Whether to enable uploading new files to Pastebin. This doesn't affect fetching already-uploaded files.</summary>
- public bool PastebinEnableUploads { get; set; }
-
}
}
diff --git a/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
index e222a235..12a5e421 100644
--- a/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
+++ b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
@@ -5,7 +5,7 @@ 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>
+ /// <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>
diff --git a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
index e5e71325..b2d8ae7e 100644
--- a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
+++ b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
@@ -6,7 +6,9 @@ using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
-using Amazon.S3.Transfer;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
using Microsoft.Extensions.Options;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.Compression;
@@ -44,54 +46,26 @@ namespace StardewModdingAPI.Web.Framework.Storage
this.GzipHelper = gzipHelper;
}
- /// <summary>Save a text file to Pastebin or Amazon S3, if available.</summary>
+ /// <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)
{
- // save to PasteBin
- string uploadError = null;
- if (this.ClientsConfig.PastebinEnableUploads)
- {
- 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
- }
- };
+ BlobClient blob = this.GetAzureBlobClient(id);
+ await blob.UploadAsync(stream);
- await uploader.UploadAsync(uploadRequest);
-
- return new UploadResult(true, id, uploadError);
+ return new UploadResult(true, id, null);
}
catch (Exception ex)
{
- return new UploadResult(false, null, $"{uploadError}\n{ex.Message}");
+ return new UploadResult(false, null, ex.Message);
}
}
@@ -99,35 +73,62 @@ namespace StardewModdingAPI.Web.Framework.Storage
/// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param>
public async Task<StoredFileInfo> GetAsync(string id)
{
- // get from Amazon S3
+ // fetch from Azure/Amazon
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 Azure
try
{
- using GetObjectResponse response = await s3.GetObjectAsync(this.ClientsConfig.AmazonTempBucket, $"uploads/{id}");
- using Stream responseStream = response.ResponseStream;
- using StreamReader reader = new StreamReader(responseStream);
+ BlobClient blob = this.GetAzureBlobClient(id);
+ Response<BlobDownloadInfo> response = await blob.DownloadAsync();
+ using BlobDownloadInfo result = response.Value;
- DateTime expiry = response.Expiration.ExpiryDateUtc;
- string pastebinError = response.Metadata["x-amz-meta-pastebin-error"];
+ using StreamReader reader = new StreamReader(result.Content);
+ DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ClientsConfig.AzureBlobTempExpiryDays);
string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
return new StoredFileInfo
{
Success = true,
Content = content,
- Expiry = expiry,
- Warning = pastebinError
+ Expiry = expiry.UtcDateTime
};
}
- catch (AmazonServiceException ex)
+ catch (RequestFailedException 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})." };
+ if (ex.ErrorCode != "BlobNotFound")
+ return new StoredFileInfo { Error = $"Could not fetch that file from storage ({ex.ErrorCode}: {ex.Message})." };
+ }
+
+ // try legacy Amazon S3
+ {
+ 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})." };
+ }
}
}
@@ -144,5 +145,14 @@ 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>
+ private BlobClient GetAzureBlobClient(string id)
+ {
+ var azure = new BlobServiceClient(this.ClientsConfig.AzureBlobConnectionString);
+ var container = azure.GetBlobContainerClient(this.ClientsConfig.AzureBlobTempContainer);
+ return container.GetBlobClient($"uploads/{id}");
+ }
}
}
diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj
index 2c56fa75..773a7316 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -13,6 +13,7 @@
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.3.108.4" />
+ <PackageReference Include="Azure.Storage.Blobs" Version="12.1.0" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.7" />
<PackageReference Include="Hangfire.Mongo" Version="0.6.5" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.16" />
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 31b5e61d..07ee0c9e 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -153,9 +153,7 @@ namespace StardewModdingAPI.Web
services.AddSingleton<IPastebinClient>(new PastebinClient(
baseUrl: api.PastebinBaseUrl,
- userAgent: userAgent,
- userKey: api.PastebinUserKey,
- devKey: api.PastebinDevKey
+ userAgent: userAgent
));
}
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json
index 74ded25d..05f0da1d 100644
--- a/src/SMAPI.Web/appsettings.Development.json
+++ b/src/SMAPI.Web/appsettings.Development.json
@@ -12,13 +12,12 @@
"AmazonAccessKey": null,
"AmazonSecretKey": null,
+ "AzureBlobConnectionString": null,
+
"GitHubUsername": null,
"GitHubPassword": null,
- "NexusApiKey": null,
-
- "PastebinUserKey": null,
- "PastebinDevKey": null
+ "NexusApiKey": null
},
"MongoDB": {
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 6ba05a22..c6dc1b69 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -29,6 +29,10 @@
"AmazonRegion": "us-east-1",
"AmazonTempBucket": "smapi-web-temp",
+ "AzureBlobConnectionString": null,
+ "AzureBlobTempContainer": "smapi-web-temp",
+ "AzureBlobTempExpiryDays": 30,
+
"ChucklefishBaseUrl": "https://community.playstarbound.com",
"ChucklefishModPageUrlFormat": "resources/{0}",
@@ -47,10 +51,7 @@
"NexusModUrlFormat": "mods/{0}",
"NexusModScrapeUrlFormat": "mods/{0}?tab=files",
- "PastebinBaseUrl": "https://pastebin.com/",
- "PastebinUserKey": null,
- "PastebinDevKey": null,
- "PastebinEnableUploads": true
+ "PastebinBaseUrl": "https://pastebin.com/"
},
"MongoDB": {