summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
blob: 12a35f189b80b96a2f81d38457e817748813dbe5 (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
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
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;
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 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)
        {
            try
            {
                using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
                string id = Guid.NewGuid().ToString("N");

                BlobClient blob = this.GetAzureBlobClient(id);
                await blob.UploadAsync(stream);

                return new UploadResult(true, id, null);
            }
            catch (Exception ex)
            {
                return new UploadResult(false, null, 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)
        {
            // fetch from Azure/Amazon
            if (Guid.TryParseExact(id, "N", out Guid _))
            {
                // try Azure
                try
                {
                    BlobClient blob = this.GetAzureBlobClient(id);
                    Response<BlobDownloadInfo> response = await blob.DownloadAsync();
                    using BlobDownloadInfo result = response.Value;

                    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.UtcDateTime
                    };
                }
                catch (RequestFailedException ex)
                {
                    return new StoredFileInfo
                    {
                        Error = ex.ErrorCode == "BlobNotFound"
                            ? "There's no file with that ID."
                            : $"Could not fetch that file from storage ({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
                };
            }
        }

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