PlanTempusAdmin/Services/AzureStorageService.cs

305 lines
10 KiB
C#
Raw Permalink Normal View History

2026-02-03 00:17:08 +01:00
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using PlanTempusAdmin.Models;
using BlobType = PlanTempusAdmin.Models.BlobType;
namespace PlanTempusAdmin.Services;
public class AzureStorageService
{
private readonly BlobServiceClient? _serviceClient;
private readonly ILogger<AzureStorageService> _logger;
private readonly string _accountName;
public AzureStorageService(IConfiguration configuration, ILogger<AzureStorageService> logger)
{
_logger = logger;
var connectionString = configuration.GetConnectionString("AzureStorage");
if (!string.IsNullOrEmpty(connectionString))
{
try
{
_serviceClient = new BlobServiceClient(connectionString);
// Extract account name from connection string
var parts = connectionString.Split(';')
.Select(p => p.Split('=', 2))
.Where(p => p.Length == 2)
.ToDictionary(p => p[0], p => p[1], StringComparer.OrdinalIgnoreCase);
_accountName = parts.TryGetValue("AccountName", out var name) ? name : "unknown";
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize Azure Storage client");
}
}
_accountName ??= string.Empty;
}
public async Task<bool> TestConnectionAsync()
{
if (_serviceClient == null) return false;
try
{
await _serviceClient.GetPropertiesAsync();
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not connect to Azure Storage");
return false;
}
}
public async Task<List<AzureContainer>> GetContainersAsync()
{
if (_serviceClient == null) return new List<AzureContainer>();
var containers = new List<AzureContainer>();
try
{
await foreach (var container in _serviceClient.GetBlobContainersAsync())
{
var containerClient = _serviceClient.GetBlobContainerClient(container.Name);
var stats = await GetContainerStatsAsync(containerClient);
containers.Add(new AzureContainer
{
Name = container.Name,
LastModified = container.Properties.LastModified,
TotalSize = stats.size,
BlobCount = stats.count
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching containers");
}
return containers;
}
public async Task<ContainerDetails> GetContainerDetailsAsync(string containerName, string? prefix = null, int limit = 100)
{
var details = new ContainerDetails { Name = containerName };
if (_serviceClient == null) return details;
try
{
var containerClient = _serviceClient.GetBlobContainerClient(containerName);
var blobs = new List<AzureBlob>();
var prefixes = new HashSet<string>();
await foreach (var item in containerClient.GetBlobsByHierarchyAsync(prefix: prefix, delimiter: "/"))
{
if (item.IsPrefix)
{
prefixes.Add(item.Prefix.TrimEnd('/'));
}
else if (item.Blob != null)
{
blobs.Add(MapToAzureBlob(item.Blob));
details.TotalSize += item.Blob.Properties.ContentLength ?? 0;
details.BlobCount++;
}
if (blobs.Count >= limit) break;
}
details.Blobs = blobs.OrderByDescending(b => b.LastModified).ToList();
details.Prefixes = prefixes.OrderBy(p => p).ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching container details for {Container}", containerName);
}
return details;
}
public async Task<List<AzureBlob>> GetBlobsAsync(string containerName, string? prefix = null, int limit = 100)
{
if (_serviceClient == null) return new List<AzureBlob>();
var blobs = new List<AzureBlob>();
try
{
var containerClient = _serviceClient.GetBlobContainerClient(containerName);
await foreach (var blob in containerClient.GetBlobsAsync(prefix: prefix))
{
blobs.Add(MapToAzureBlob(blob));
if (blobs.Count >= limit) break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching blobs from {Container}", containerName);
}
return blobs.OrderByDescending(b => b.LastModified).ToList();
}
public async Task<AzureStorageDashboard> GetDashboardAsync()
{
var dashboard = new AzureStorageDashboard
{
AccountName = _accountName
};
if (_serviceClient == null) return dashboard;
try
{
dashboard.IsConnected = await TestConnectionAsync();
if (!dashboard.IsConnected) return dashboard;
var containers = await GetContainersAsync();
dashboard.Containers = containers;
dashboard.TotalContainers = containers.Count;
dashboard.TotalSize = containers.Sum(c => c.TotalSize);
dashboard.TotalBlobs = containers.Sum(c => c.BlobCount);
// Find backup container(s) and get recent backups
var backupContainers = containers.Where(c =>
c.Name.Contains("backup", StringComparison.OrdinalIgnoreCase) ||
c.Name.Contains("backups", StringComparison.OrdinalIgnoreCase)).ToList();
var recentBlobs = new List<AzureBlob>();
foreach (var container in backupContainers.Take(3))
{
var blobs = await GetBlobsAsync(container.Name, limit: 20);
recentBlobs.AddRange(blobs);
var backupBlobs = blobs.Where(b => b.IsBackup).ToList();
dashboard.BackupFileCount += backupBlobs.Count;
dashboard.BackupTotalSize += backupBlobs.Sum(b => b.Size);
}
dashboard.RecentBlobs = recentBlobs
.OrderByDescending(b => b.LastModified)
.Take(10)
.ToList();
if (dashboard.RecentBlobs.Any())
{
dashboard.LastBackupUpload = dashboard.RecentBlobs
.Where(b => b.IsBackup)
.Max(b => b.LastModified);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error building Azure Storage dashboard");
}
return dashboard;
}
public async Task<string?> GetBlobDownloadUrlAsync(string containerName, string blobName, TimeSpan? expiry = null)
{
if (_serviceClient == null) return null;
try
{
var containerClient = _serviceClient.GetBlobContainerClient(containerName);
var blobClient = containerClient.GetBlobClient(blobName);
if (!await blobClient.ExistsAsync()) return null;
// Generate SAS token for download
var sasBuilder = new Azure.Storage.Sas.BlobSasBuilder
{
BlobContainerName = containerName,
BlobName = blobName,
Resource = "b",
ExpiresOn = DateTimeOffset.UtcNow.Add(expiry ?? TimeSpan.FromHours(1))
};
sasBuilder.SetPermissions(Azure.Storage.Sas.BlobSasPermissions.Read);
// Check if we can generate SAS (requires account key)
if (blobClient.CanGenerateSasUri)
{
return blobClient.GenerateSasUri(sasBuilder).ToString();
}
return blobClient.Uri.ToString();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating download URL for {Container}/{Blob}", containerName, blobName);
return null;
}
}
public async Task<bool> DeleteBlobAsync(string containerName, string blobName)
{
if (_serviceClient == null) return false;
try
{
var containerClient = _serviceClient.GetBlobContainerClient(containerName);
var blobClient = containerClient.GetBlobClient(blobName);
var response = await blobClient.DeleteIfExistsAsync();
return response.Value;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting blob {Container}/{Blob}", containerName, blobName);
return false;
}
}
private async Task<(long size, int count)> GetContainerStatsAsync(BlobContainerClient containerClient)
{
long totalSize = 0;
int count = 0;
try
{
await foreach (var blob in containerClient.GetBlobsAsync())
{
totalSize += blob.Properties.ContentLength ?? 0;
count++;
// Limit iteration for large containers
if (count >= 10000) break;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error getting stats for container {Container}", containerClient.Name);
}
return (totalSize, count);
}
private static AzureBlob MapToAzureBlob(BlobItem blob)
{
return new AzureBlob
{
Name = blob.Name,
ContentType = blob.Properties.ContentType,
Size = blob.Properties.ContentLength ?? 0,
LastModified = blob.Properties.LastModified,
CreatedOn = blob.Properties.CreatedOn,
AccessTier = blob.Properties.AccessTier?.ToString(),
BlobType = blob.Properties.BlobType switch
{
Azure.Storage.Blobs.Models.BlobType.Page => BlobType.Page,
Azure.Storage.Blobs.Models.BlobType.Append => BlobType.Append,
_ => BlobType.Block
}
};
}
}