using System;
using System.IO;
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.Extensions.Options;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.Compression;
using StardewModdingAPI.Web.Framework.ConfigModels;
namespace StardewModdingAPI.Web.Framework.Storage
{
/// Provides access to raw data storage.
internal class StorageProvider : IStorageProvider
{
/*********
** Fields
*********/
/// The API client settings.
private readonly ApiClientsConfig ClientsConfig;
/// The underlying Pastebin client.
private readonly IPastebinClient Pastebin;
/// The underlying text compression helper.
private readonly IGzipHelper GzipHelper;
/*********
** Public methods
*********/
/// Construct an instance.
/// The API client settings.
/// The underlying Pastebin client.
/// The underlying text compression helper.
public StorageProvider(IOptions clientsConfig, IPastebinClient pastebin, IGzipHelper gzipHelper)
{
this.ClientsConfig = clientsConfig.Value;
this.Pastebin = pastebin;
this.GzipHelper = gzipHelper;
}
/// Save a text file to Pastebin or Amazon S3, if available.
/// The display title, if applicable.
/// The content to upload.
/// Whether to gzip the text.
/// Returns metadata about the save attempt.
public async Task SaveAsync(string title, string content, bool compress = true)
{
// save to PasteBin
string uploadError;
{
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
}
};
await uploader.UploadAsync(uploadRequest);
return new UploadResult(true, id, uploadError);
}
catch (Exception ex)
{
return new UploadResult(false, null, $"{uploadError}\n{ex.Message}");
}
}
/// Fetch raw text from storage.
/// The storage ID returned by .
public async Task GetAsync(string id)
{
// 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.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})." };
}
}
// get from PasteBin
else
{
PasteInfo response = await this.Pastebin.GetAsync(id);
response.Content = this.GzipHelper.DecompressString(response.Content);
return new StoredFileInfo
{
Success = response.Success,
Content = response.Content,
Error = response.Error
};
}
}
}
}