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 _logger; private readonly string _accountName; public AzureStorageService(IConfiguration configuration, ILogger 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 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> GetContainersAsync() { if (_serviceClient == null) return new List(); var containers = new List(); 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 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(); var prefixes = new HashSet(); 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> GetBlobsAsync(string containerName, string? prefix = null, int limit = 100) { if (_serviceClient == null) return new List(); var blobs = new List(); 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 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(); 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 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 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 } }; } }