Initial commit
This commit is contained in:
commit
77d35ff965
51 changed files with 5591 additions and 0 deletions
304
Services/AzureStorageService.cs
Normal file
304
Services/AzureStorageService.cs
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue