304 lines
10 KiB
C#
304 lines
10 KiB
C#
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
|
|
}
|
|
};
|
|
}
|
|
}
|