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