using System; using System.Collections.Generic; 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 { /// 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; /// Whether Azure blob storage is configured. private bool HasAzure => !string.IsNullOrWhiteSpace(this.ClientsConfig.AzureBlobConnectionString); /// The number of days since the blob's last-modified date when it will be deleted. private int ExpiryDays => this.ClientsConfig.AzureBlobTempExpiryDays; /********* ** 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; } /// public async Task SaveAsync(string content, bool compress = true) { string id = Guid.NewGuid().ToString("N"); // save to Azure if (this.HasAzure) { try { using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); BlobClient blob = this.GetAzureBlobClient(id); await blob.UploadAsync(stream); return new UploadResult(id, null); } catch (Exception ex) { return new UploadResult(null, ex.Message); } } // save to local filesystem for testing else { string path = this.GetDevFilePath(id); Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, content); return new UploadResult(id, null); } } /// public async Task GetAsync(string id, bool renew) { // fetch from blob storage if (Guid.TryParseExact(id, "N", out Guid _)) { // Azure Blob storage if (this.HasAzure) { try { // get client BlobClient blob = this.GetAzureBlobClient(id); // extend expiry if (renew) await blob.SetMetadataAsync(new Dictionary { ["expiryRenewed"] = DateTime.UtcNow.ToString("O") }); // change the blob's last-modified date (the specific property set doesn't matter) // fetch file Response response = await blob.DownloadAsync(); using BlobDownloadInfo result = response.Value; using StreamReader reader = new(result.Content); DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ExpiryDays); string content = this.GzipHelper.DecompressString(reader.ReadToEnd()); // build model return new StoredFileInfo(content, expiry); } 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})." ); } } // local filesystem for testing else { // get file FileInfo file = new(this.GetDevFilePath(id)); if (file.Exists && file.LastWriteTimeUtc.AddDays(this.ExpiryDays) < DateTime.UtcNow) // expired file.Delete(); if (!file.Exists) { return new StoredFileInfo(error: "There's no file with that ID."); } // renew if (renew) { File.SetLastWriteTimeUtc(file.FullName, DateTime.UtcNow); file.Refresh(); } // build model return new StoredFileInfo( content: File.ReadAllText(file.FullName), expiry: DateTime.UtcNow.AddDays(this.ExpiryDays), warning: "This file was saved temporarily to the local computer. This should only happen in a local development environment." ); } } // get from Pastebin else { PasteInfo response = await this.Pastebin.GetAsync(id); response.Content = this.GzipHelper.DecompressString(response.Content); return new StoredFileInfo(response.Content, null, error: response.Error); } } /// Get a client for reading and writing to Azure Blob storage. /// The file ID. private BlobClient GetAzureBlobClient(string id) { BlobServiceClient azure = new(this.ClientsConfig.AzureBlobConnectionString); BlobContainerClient container = azure.GetBlobContainerClient(this.ClientsConfig.AzureBlobTempContainer); return container.GetBlobClient($"uploads/{id}"); } /// Get the absolute file path for an upload when running in a local test environment with no Azure account configured. /// The file ID. private string GetDevFilePath(string id) { return Path.Combine(Path.GetTempPath(), "smapi-web-temp", $"{id}.txt"); } } }