#nullable disable
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(true, id, null);
}
catch (Exception ex)
{
return new UploadResult(false, 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(true, 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
{
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})."
};
}
}
// 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
{
Success = true,
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
{
Success = response.Success,
Content = response.Content,
Error = response.Error
};
}
}
/// Get a client for reading and writing to Azure Blob storage.
/// The file ID.
private BlobClient GetAzureBlobClient(string id)
{
var azure = new BlobServiceClient(this.ClientsConfig.AzureBlobConnectionString);
var 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");
}
}
}