summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
blob: bbb6e06b85e6e1bc457e95b5c2f5f499fb1c481a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
{
    /// <summary>Provides access to raw data storage.</summary>
    internal class StorageProvider : IStorageProvider
    {
        /*********
        ** Fields
        *********/
        /// <summary>The API client settings.</summary>
        private readonly ApiClientsConfig ClientsConfig;

        /// <summary>The underlying Pastebin client.</summary>
        private readonly IPastebinClient Pastebin;

        /// <summary>The underlying text compression helper.</summary>
        private readonly IGzipHelper GzipHelper;


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="clientsConfig">The API client settings.</param>
        /// <param name="pastebin">The underlying Pastebin client.</param>
        /// <param name="gzipHelper">The underlying text compression helper.</param>
        public StorageProvider(IOptions<ApiClientsConfig> clientsConfig, IPastebinClient pastebin, IGzipHelper gzipHelper)
        {
            this.ClientsConfig = clientsConfig.Value;
            this.Pastebin = pastebin;
            this.GzipHelper = gzipHelper;
        }

        /// <summary>Save a text file to Pastebin or Amazon S3, if available.</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;
            {
                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}");
            }
        }

        /// <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)
        {
            // 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
                };
            }
        }
    }
}