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 }; } } } }