mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-07 08:29:51 +08:00
feat(storage): implement storage management API and enhance storage mode handling
This commit is contained in:
19
Services/Management/IStorageManagementService.cs
Normal file
19
Services/Management/IStorageManagementService.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Foxel.Models; // For PaginatedResult
|
||||
using Foxel.Models.Request.Storage;
|
||||
using Foxel.Models.Response.Storage;
|
||||
using Foxel.Services.Attributes; // For StorageType
|
||||
|
||||
namespace Foxel.Services.Management;
|
||||
|
||||
public interface IStorageManagementService
|
||||
{
|
||||
Task<PaginatedResult<StorageModeResponse>> GetStorageModesAsync(int page = 1, int pageSize = 10, string? searchQuery = null, StorageType? storageType = null, bool? isEnabled = null);
|
||||
Task<StorageModeResponse> GetStorageModeByIdAsync(int id);
|
||||
Task<StorageModeResponse> CreateStorageModeAsync(CreateStorageModeRequest request);
|
||||
Task<StorageModeResponse> UpdateStorageModeAsync(UpdateStorageModeRequest request);
|
||||
Task<bool> DeleteStorageModeAsync(int id);
|
||||
Task<BatchDeleteResult> BatchDeleteStorageModesAsync(List<int> ids);
|
||||
Task<IEnumerable<StorageTypeResponse>> GetStorageTypesAsync();
|
||||
Task<int?> GetDefaultStorageModeIdAsync();
|
||||
Task<bool> SetDefaultStorageModeAsync(int storageModeId);
|
||||
}
|
||||
@@ -48,10 +48,10 @@ public class PictureManagementService(
|
||||
{
|
||||
Id = picture.Id,
|
||||
Name = picture.Name,
|
||||
Path = storageService.ExecuteAsync(picture.StorageType, provider =>
|
||||
Task.FromResult(provider.GetUrl(picture.Path))).Result,
|
||||
ThumbnailPath = storageService.ExecuteAsync(picture.StorageType, provider =>
|
||||
Task.FromResult(provider.GetUrl(picture.ThumbnailPath ?? string.Empty))).Result,
|
||||
Path = storageService.ExecuteAsync(picture.StorageModeId, provider =>
|
||||
Task.FromResult(provider.GetUrl(picture.Id,picture.Path))).Result,
|
||||
ThumbnailPath = storageService.ExecuteAsync(picture.StorageModeId, provider =>
|
||||
Task.FromResult(provider.GetUrl(picture.Id,picture.ThumbnailPath ?? string.Empty))).Result,
|
||||
Description = picture.Description,
|
||||
CreatedAt = picture.CreatedAt,
|
||||
TakenAt = picture.TakenAt,
|
||||
@@ -119,7 +119,7 @@ public class PictureManagementService(
|
||||
// 保存文件路径信息用于后续删除
|
||||
var filePath = picture.Path;
|
||||
var thumbnailPath = picture.ThumbnailPath;
|
||||
var storageType = picture.StorageType;
|
||||
var storageType = picture.StorageModeId;
|
||||
|
||||
// 删除数据库记录
|
||||
dbContext.Pictures.Remove(picture);
|
||||
|
||||
310
Services/Management/StorageManagementService.cs
Normal file
310
Services/Management/StorageManagementService.cs
Normal file
@@ -0,0 +1,310 @@
|
||||
using Foxel.Models;
|
||||
using Foxel.Models.DataBase;
|
||||
using Foxel.Models.Request.Storage;
|
||||
using Foxel.Models.Response.Storage;
|
||||
using Foxel.Services.Attributes;
|
||||
using Foxel.Services.Storage.Providers; // Required for config types like LocalStorageConfig, etc.
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
using Foxel.Services.Configuration; // Added for IConfigService
|
||||
|
||||
namespace Foxel.Services.Management;
|
||||
|
||||
public class StorageManagementService : IStorageManagementService
|
||||
{
|
||||
private readonly IDbContextFactory<MyDbContext> _contextFactory;
|
||||
private readonly ILogger<StorageManagementService> _logger;
|
||||
private readonly IConfigService _configService; // Added IConfigService
|
||||
private const string DefaultStorageModeIdKey = "Storage:DefaultStorageModeId"; // Define key for config
|
||||
|
||||
public StorageManagementService(
|
||||
IDbContextFactory<MyDbContext> contextFactory,
|
||||
ILogger<StorageManagementService> logger,
|
||||
IConfigService configService) // Added IConfigService to constructor
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
_configService = configService; // Initialize IConfigService
|
||||
}
|
||||
|
||||
public async Task<PaginatedResult<StorageModeResponse>> GetStorageModesAsync(int page = 1, int pageSize = 10, string? searchQuery = null, StorageType? storageType = null, bool? isEnabled = null)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 10;
|
||||
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
var query = dbContext.StorageModes.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchQuery))
|
||||
{
|
||||
query = query.Where(sm => sm.Name.Contains(searchQuery));
|
||||
}
|
||||
if (storageType.HasValue)
|
||||
{
|
||||
query = query.Where(sm => sm.StorageType == storageType.Value);
|
||||
}
|
||||
if (isEnabled.HasValue)
|
||||
{
|
||||
query = query.Where(sm => sm.IsEnabled == isEnabled.Value);
|
||||
}
|
||||
|
||||
query = query.OrderByDescending(sm => sm.CreatedAt);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var storageModes = await query
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var responseItems = storageModes.Select(sm => new StorageModeResponse
|
||||
{
|
||||
Id = sm.Id,
|
||||
Name = sm.Name,
|
||||
StorageType = sm.StorageType,
|
||||
ConfigurationJson = sm.ConfigurationJson, // Consider masking sensitive info if necessary
|
||||
IsEnabled = sm.IsEnabled,
|
||||
CreatedAt = sm.CreatedAt,
|
||||
UpdatedAt = sm.UpdatedAt
|
||||
}).ToList();
|
||||
|
||||
return new PaginatedResult<StorageModeResponse>
|
||||
{
|
||||
Data = responseItems,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<StorageModeResponse> GetStorageModeByIdAsync(int id)
|
||||
{
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
var sm = await dbContext.StorageModes.FindAsync(id);
|
||||
if (sm == null)
|
||||
throw new KeyNotFoundException($"StorageMode with ID {id} not found.");
|
||||
|
||||
return new StorageModeResponse
|
||||
{
|
||||
Id = sm.Id,
|
||||
Name = sm.Name,
|
||||
StorageType = sm.StorageType,
|
||||
ConfigurationJson = sm.ConfigurationJson,
|
||||
IsEnabled = sm.IsEnabled,
|
||||
CreatedAt = sm.CreatedAt,
|
||||
UpdatedAt = sm.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<StorageModeResponse> CreateStorageModeAsync(CreateStorageModeRequest request)
|
||||
{
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
ValidateConfiguration(request.StorageType, request.ConfigurationJson, request.Name);
|
||||
|
||||
var storageMode = new Models.DataBase.StorageMode
|
||||
{
|
||||
Name = request.Name,
|
||||
StorageType = request.StorageType,
|
||||
ConfigurationJson = request.ConfigurationJson,
|
||||
IsEnabled = request.IsEnabled,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
dbContext.StorageModes.Add(storageMode);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return await GetStorageModeByIdAsync(storageMode.Id); // Reuse to get full response model
|
||||
}
|
||||
|
||||
public async Task<StorageModeResponse> UpdateStorageModeAsync(UpdateStorageModeRequest request)
|
||||
{
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
var storageMode = await dbContext.StorageModes.FindAsync(request.Id);
|
||||
|
||||
if (storageMode == null)
|
||||
throw new KeyNotFoundException($"StorageMode with ID {request.Id} not found.");
|
||||
|
||||
ValidateConfiguration(request.StorageType, request.ConfigurationJson, request.Name);
|
||||
|
||||
storageMode.Name = request.Name;
|
||||
storageMode.StorageType = request.StorageType;
|
||||
storageMode.ConfigurationJson = request.ConfigurationJson;
|
||||
storageMode.IsEnabled = request.IsEnabled;
|
||||
storageMode.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
return await GetStorageModeByIdAsync(storageMode.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteStorageModeAsync(int id)
|
||||
{
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
var storageMode = await dbContext.StorageModes.FindAsync(id);
|
||||
if (storageMode == null)
|
||||
throw new KeyNotFoundException($"StorageMode with ID {id} not found.");
|
||||
|
||||
// Check if any pictures are using this storage mode
|
||||
bool isInUse = await dbContext.Pictures.AnyAsync(p => p.StorageModeId == id);
|
||||
if (isInUse)
|
||||
{
|
||||
_logger.LogWarning("Attempted to delete StorageMode ID {StorageModeId} which is currently in use by one or more pictures.", id);
|
||||
throw new InvalidOperationException($"StorageMode '{storageMode.Name}' (ID: {id}) cannot be deleted because it is currently in use by one or more pictures.");
|
||||
}
|
||||
|
||||
dbContext.StorageModes.Remove(storageMode);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<BatchDeleteResult> BatchDeleteStorageModesAsync(List<int> ids)
|
||||
{
|
||||
var result = new BatchDeleteResult();
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var storageMode = await dbContext.StorageModes.FindAsync(id);
|
||||
if (storageMode == null)
|
||||
{
|
||||
result.FailedCount++;
|
||||
result.FailedIds.Add(id);
|
||||
_logger.LogWarning("Batch delete: StorageMode with ID {StorageModeId} not found.", id);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isInUse = await dbContext.Pictures.AnyAsync(p => p.StorageModeId == id);
|
||||
if (isInUse)
|
||||
{
|
||||
result.FailedCount++;
|
||||
result.FailedIds.Add(id);
|
||||
_logger.LogWarning("Batch delete: StorageMode ID {StorageModeId} ('{StorageModeName}') is in use and cannot be deleted.", id, storageMode.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
dbContext.StorageModes.Remove(storageMode);
|
||||
result.SuccessCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.FailedCount++;
|
||||
result.FailedIds.Add(id);
|
||||
_logger.LogError(ex, "Batch delete: Error deleting StorageMode ID {StorageModeId}.", id);
|
||||
}
|
||||
}
|
||||
if (result.SuccessCount > 0)
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<StorageTypeResponse>> GetStorageTypesAsync()
|
||||
{
|
||||
var types = Enum.GetValues(typeof(StorageType))
|
||||
.Cast<StorageType>()
|
||||
.Select(e => new StorageTypeResponse { Value = (int)e, Name = e.ToString() })
|
||||
.ToList();
|
||||
return Task.FromResult<IEnumerable<StorageTypeResponse>>(types);
|
||||
}
|
||||
|
||||
public async Task<int?> GetDefaultStorageModeIdAsync()
|
||||
{
|
||||
var idString = await _configService.GetValueAsync(DefaultStorageModeIdKey);
|
||||
if (int.TryParse(idString, out var id))
|
||||
{
|
||||
// Optionally, verify if this ID still exists and is enabled in StorageModes table
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
var storageModeExists = await dbContext.StorageModes.AnyAsync(sm => sm.Id == id && sm.IsEnabled);
|
||||
if (storageModeExists)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
_logger.LogWarning("Default storage mode ID {DefaultStorageModeId} from config does not exist or is not enabled.", id);
|
||||
// If it doesn't exist or isn't enabled, perhaps clear the setting or return null
|
||||
// For now, returning null as it's not a valid/usable default
|
||||
await _configService.DeleteConfigAsync(DefaultStorageModeIdKey); // Clean up invalid setting
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<bool> SetDefaultStorageModeAsync(int storageModeId)
|
||||
{
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
var storageMode = await dbContext.StorageModes.FindAsync(storageModeId);
|
||||
|
||||
if (storageMode == null)
|
||||
{
|
||||
_logger.LogWarning("Attempted to set default storage mode to a non-existent ID: {StorageModeId}", storageModeId);
|
||||
throw new KeyNotFoundException($"StorageMode with ID {storageModeId} not found.");
|
||||
}
|
||||
|
||||
if (!storageMode.IsEnabled)
|
||||
{
|
||||
_logger.LogWarning("Attempted to set default storage mode to a disabled StorageMode ID: {StorageModeId}, Name: {StorageModeName}", storageModeId, storageMode.Name);
|
||||
throw new InvalidOperationException($"StorageMode '{storageMode.Name}' (ID: {storageModeId}) is disabled and cannot be set as default.");
|
||||
}
|
||||
|
||||
// Validate configuration before setting as default
|
||||
try
|
||||
{
|
||||
ValidateConfiguration(storageMode.StorageType, storageMode.ConfigurationJson, storageMode.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Validation failed for StorageMode ID {StorageModeId} ('{StorageModeName}') when trying to set as default. Configuration: {ConfigurationJson}", storageModeId, storageMode.Name, storageMode.ConfigurationJson);
|
||||
throw new InvalidOperationException($"StorageMode '{storageMode.Name}' (ID: {storageModeId}) has an invalid configuration and cannot be set as default. Error: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
|
||||
await _configService.SetConfigAsync(DefaultStorageModeIdKey, storageModeId.ToString(), "The ID of the default storage mode to be used by the application.");
|
||||
_logger.LogInformation("Default storage mode set to ID: {StorageModeId}, Name: {StorageModeName}", storageModeId, storageMode.Name);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ValidateConfiguration(StorageType storageType, string? jsonConfig, string storageModeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jsonConfig))
|
||||
{
|
||||
// Configuration can be optional for some types or scenarios,
|
||||
// but if a type inherently requires config, this check might need adjustment.
|
||||
// For now, we assume if jsonConfig is null/empty, it's intentional.
|
||||
// The actual provider instantiation will fail if config is required but missing.
|
||||
// This validation step is more about format if config IS provided.
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object? deserializedConfig = storageType switch
|
||||
{
|
||||
StorageType.Local => JsonSerializer.Deserialize<LocalStorageConfig>(jsonConfig),
|
||||
StorageType.Telegram => JsonSerializer.Deserialize<TelegramStorageConfig>(jsonConfig),
|
||||
StorageType.S3 => JsonSerializer.Deserialize<S3StorageConfig>(jsonConfig),
|
||||
StorageType.Cos => JsonSerializer.Deserialize<CosStorageConfig>(jsonConfig),
|
||||
StorageType.WebDAV => JsonSerializer.Deserialize<WebDavStorageConfig>(jsonConfig),
|
||||
_ => throw new NotSupportedException($"StorageType {storageType} configuration validation is not supported.")
|
||||
};
|
||||
|
||||
if (deserializedConfig == null)
|
||||
{
|
||||
throw new JsonException($"Unable to deserialize configuration for {storageType}. JSON: {jsonConfig}");
|
||||
}
|
||||
// Further property-level validation can be added here if needed (e.g., checking required fields within the config object)
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Invalid JSON configuration for StorageMode '{StorageModeName}' (Type: {StorageType}). JSON: {JsonConfig}", storageModeName, storageType, jsonConfig);
|
||||
throw new ArgumentException($"Configuration for StorageMode '{storageModeName}' (Type: {storageType}) is invalid: {ex.Message}", nameof(jsonConfig), ex);
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Validation not supported for StorageType '{StorageType}' in StorageMode '{StorageModeName}'.", storageType, storageModeName);
|
||||
throw new ArgumentException(ex.Message, nameof(storageType), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user