feat(storage): implement storage management API and enhance storage mode handling

This commit is contained in:
shiyu
2025-06-09 12:12:15 +08:00
parent 4ef4b2056b
commit 0a6fe70537
43 changed files with 2449 additions and 907 deletions

View File

@@ -0,0 +1,193 @@
using Foxel.Controllers;
using Foxel.Models;
using Foxel.Models.Request.Storage;
using Foxel.Models.Response.Storage;
using Foxel.Services.Attributes;
using Foxel.Services.Management;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Foxel.Api.Management;
[Authorize(Roles = "Administrator")]
[Route("api/management/storage")]
public class StorageManagementController(IStorageManagementService storageManagementService) : BaseApiController
{
[HttpGet("get_modes")]
public async Task<ActionResult<PaginatedResult<StorageModeResponse>>> GetStorageModes(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? searchQuery = null,
[FromQuery] StorageType? storageType = null,
[FromQuery] bool? isEnabled = null)
{
try
{
var result =
await storageManagementService.GetStorageModesAsync(page, pageSize, searchQuery, storageType,
isEnabled);
return PaginatedSuccess(result.Data, result.TotalCount, result.Page, result.PageSize);
}
catch (Exception ex)
{
return PaginatedError<StorageModeResponse>($"Failed to get storage modes: {ex.Message}", 500);
}
}
[HttpGet("get_mode/{id}")]
public async Task<ActionResult<BaseResult<StorageModeResponse>>> GetStorageModeById(int id)
{
try
{
var result = await storageManagementService.GetStorageModeByIdAsync(id);
return Success(result, "Storage mode retrieved successfully.");
}
catch (KeyNotFoundException ex)
{
return Error<StorageModeResponse>(ex.Message, 404);
}
catch (Exception ex)
{
return Error<StorageModeResponse>($"Failed to get storage mode: {ex.Message}", 500);
}
}
[HttpPost("create_mode")]
public async Task<ActionResult<BaseResult<StorageModeResponse>>> CreateStorageMode(
[FromBody] CreateStorageModeRequest request)
{
try
{
var result = await storageManagementService.CreateStorageModeAsync(request);
return Success(result, "Storage mode created successfully.");
}
catch (ArgumentException ex)
{
return Error<StorageModeResponse>(ex.Message, 400);
}
catch (Exception ex)
{
return Error<StorageModeResponse>($"Failed to create storage mode: {ex.Message}", 500);
}
}
[HttpPost("update_mode")]
public async Task<ActionResult<BaseResult<StorageModeResponse>>> UpdateStorageMode(
[FromBody] UpdateStorageModeRequest request)
{
try
{
var result = await storageManagementService.UpdateStorageModeAsync(request);
return Success(result, "Storage mode updated successfully.");
}
catch (KeyNotFoundException ex)
{
return Error<StorageModeResponse>(ex.Message, 404);
}
catch (ArgumentException ex)
{
return Error<StorageModeResponse>(ex.Message, 400);
}
catch (Exception ex)
{
return Error<StorageModeResponse>($"Failed to update storage mode: {ex.Message}", 500);
}
}
[HttpPost("delete_mode")]
public async Task<ActionResult<BaseResult<bool>>> DeleteStorageMode([FromBody] int id)
{
try
{
var result = await storageManagementService.DeleteStorageModeAsync(id);
return Success(result, "Storage mode deleted successfully.");
}
catch (KeyNotFoundException ex)
{
return Error<bool>(ex.Message, 404);
}
catch (InvalidOperationException ex) // Catch specific exception for "in use"
{
return Error<bool>(ex.Message, 400);
}
catch (Exception ex)
{
return Error<bool>($"Failed to delete storage mode: {ex.Message}", 500);
}
}
[HttpPost("batch_delete_modes")]
public async Task<ActionResult<BaseResult<BatchDeleteResult>>> BatchDeleteStorageModes([FromBody] List<int> ids)
{
try
{
if (ids == null || !ids.Any())
{
return Error<BatchDeleteResult>("No IDs provided for batch deletion.", 400);
}
var result = await storageManagementService.BatchDeleteStorageModesAsync(ids);
return Success(result,
$"Batch delete completed. Succeeded: {result.SuccessCount}, Failed: {result.FailedCount}.");
}
catch (Exception ex)
{
return Error<BatchDeleteResult>($"Batch delete failed: {ex.Message}", 500);
}
}
[HttpGet("get_storage_types")]
public async Task<ActionResult<BaseResult<IEnumerable<StorageTypeResponse>>>> GetStorageTypes()
{
try
{
var result = await storageManagementService.GetStorageTypesAsync();
return Success(result, "Storage types retrieved successfully.");
}
catch (Exception ex)
{
return Error<IEnumerable<StorageTypeResponse>>($"Failed to get storage types: {ex.Message}", 500);
}
}
[HttpGet("get_default_mode_id")]
public async Task<ActionResult<BaseResult<int?>>> GetDefaultStorageModeId()
{
try
{
var result = await storageManagementService.GetDefaultStorageModeIdAsync();
if (result.HasValue)
{
return Success<int?>(result.Value, "Default storage mode ID retrieved successfully.");
}
return Success<int?>(null, "No default storage mode is currently set or the configured one is invalid.");
}
catch (Exception ex)
{
return Error<int?>($"Failed to get default storage mode ID: {ex.Message}", 500);
}
}
[HttpPost("set_default_mode/{id}")]
public async Task<ActionResult<BaseResult<bool>>> SetDefaultStorageMode(int id)
{
try
{
var result = await storageManagementService.SetDefaultStorageModeAsync(id);
return Success(result, $"Default storage mode set to ID {id} successfully.");
}
catch (KeyNotFoundException ex)
{
return Error<bool>(ex.Message, 404);
}
catch (InvalidOperationException ex)
{
return Error<bool>(ex.Message, 400);
}
catch (Exception ex)
{
return Error<bool>($"Failed to set default storage mode: {ex.Message}", 500);
}
}
}

View File

@@ -1,22 +1,23 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization;
using Foxel.Controllers;
using Foxel.Models;
using Foxel.Models.DataBase;
using Foxel.Models.Request.Picture;
using Foxel.Models.Response.Picture;
using System.Text.Json;
using System.Text.Json.Serialization;
using Foxel.Services.Media;
using Foxel.Services.Storage;
using System.IO;
using Foxel.Services.Attributes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json; // Added for JsonSerializer in GetTelegramFile if it were kept
namespace Foxel.Controllers;
namespace Foxel.Api;
[Authorize]
[Route("api/picture")]
public class PictureController(IPictureService pictureService, IStorageService storageService) : BaseApiController
public class PictureController(IPictureService pictureService, IStorageService storageService, ILogger<PictureController> logger) : BaseApiController
{
private readonly ILogger<PictureController> _logger = logger;
[HttpGet("get_pictures")]
public async Task<ActionResult<PaginatedResult<PictureResponse>>> GetPictures(
[FromQuery] FilteredPicturesRequest request)
@@ -81,7 +82,7 @@ public class PictureController(IPictureService pictureService, IStorageService s
userId,
(PermissionType)request.Permission!,
request.AlbumId,
request.StorageType
request.StorageModeId
);
var picture = result.Picture;
@@ -260,92 +261,76 @@ public class PictureController(IPictureService pictureService, IStorageService s
}
}
[HttpGet("get_telegram_file")]
[HttpGet("file/{pictureId}")]
[AllowAnonymous]
public async Task<IActionResult> GetTelegramFile([FromQuery] string fileId)
public async Task<IActionResult> GetPictureFile(int pictureId)
{
try
{
// 创建一个模拟的存储元数据
var metadata = new
var picture = await pictureService.GetPictureByIdAsync(pictureId);
if (picture == null)
{
FileId = fileId,
OriginalFileName = "telegram_file"
};
// 序列化为 JSON 字符串,与 TelegramStorageProvider 中的格式保持一致
string storagePath = JsonSerializer.Serialize(metadata);
try
{
// 使用 storageService 下载文件,这样会自动使用配置的代理
string tempFilePath = await storageService.ExecuteAsync(StorageType.Telegram,
provider => provider.DownloadFileAsync(storagePath));
// 获取文件内容类型
string contentType = GetContentTypeFromPath(tempFilePath);
// 返回文件
return PhysicalFile(tempFilePath, contentType, Path.GetFileName(tempFilePath));
_logger.LogWarning("GetPictureFile: Picture with ID {PictureId} not found.", pictureId);
return NotFound("Picture not found.");
}
catch (Exception ex)
var currentUserId = GetCurrentUserId();
if (picture.Permission != PermissionType.Public)
{
return StatusCode(500, $"下载 Telegram 文件失败: {ex.Message}");
if (currentUserId == null || picture.UserId != currentUserId.Value)
{
_logger.LogWarning("GetPictureFile: User {UserId} forbidden to access picture {PictureId}.", currentUserId, pictureId);
return Forbid();
}
}
// 3. 使用 StorageService 下载文件
string tempFilePath = await storageService.ExecuteAsync(
picture.StorageModeId,
provider => provider.DownloadFileAsync(picture.Path)
);
if (string.IsNullOrEmpty(tempFilePath) || !System.IO.File.Exists(tempFilePath))
{
_logger.LogError("GetPictureFile: Failed to download file or file not found at temp path for picture ID {PictureId}. TempPath: {TempPath}", pictureId, tempFilePath);
return StatusCode(500, "Failed to retrieve file from storage.");
}
// 4. 确定内容类型
string contentType = GetContentTypeFromPath(tempFilePath);
// 5. 返回文件
return PhysicalFile(tempFilePath, contentType, Path.GetFileName(picture.Name));
}
catch (KeyNotFoundException knfEx)
{
_logger.LogWarning(knfEx, "GetPictureFile: Resource not found for picture ID {PictureId}.", pictureId);
return NotFound($"Resource related to picture ID {pictureId} not found.");
}
catch (FileNotFoundException fnfEx)
{
_logger.LogWarning(fnfEx, "GetPictureFile: File not found in storage for picture ID {PictureId}.", pictureId);
return NotFound("File not found in storage.");
}
catch (NotImplementedException niEx)
{
_logger.LogError(niEx, "GetPictureFile: DownloadFileAsync not implemented for the storage provider of picture ID {PictureId}.", pictureId);
return StatusCode(501, "File download is not supported for this storage type.");
}
catch (InvalidOperationException ioEx)
{
_logger.LogError(ioEx, "GetPictureFile: Invalid operation for picture ID {PictureId}.", pictureId);
return StatusCode(500, $"Error processing file request: {ioEx.Message}");
}
catch (Exception ex)
{
return StatusCode(500, $"代理获取文件失败: {ex.Message}");
}
}
// 用于解析 Telegram getFile API 响应的辅助类
private class TelegramGetFileResponse
{
[JsonPropertyName("ok")]
public bool Ok { get; set; }
[JsonPropertyName("result")]
public TelegramFileResult? Result { get; set; }
}
private class TelegramFileResult
{
[JsonPropertyName("file_path")]
public string? FilePath { get; set; }
}
[HttpGet("proxy")]
[AllowAnonymous]
public async Task<IActionResult> GetWebDavFile([FromQuery] string path)
{
try
{
if (string.IsNullOrEmpty(path))
{
return BadRequest("文件路径不能为空");
}
// 下载文件到临时位置
string filePath = await storageService.ExecuteAsync(StorageType.WebDAV,
provider => provider.DownloadFileAsync(path));
// 确定内容类型
string contentType = GetContentTypeFromPath(path);
// 返回文件内容
return PhysicalFile(filePath, contentType, Path.GetFileName(path));
}
catch (Exception ex)
{
return StatusCode(500, $"代理获取WebDAV文件失败: {ex.Message}");
_logger.LogError(ex, "GetPictureFile: Error getting file for picture ID {PictureId}", pictureId);
return StatusCode(500, "An error occurred while retrieving the file.");
}
}
private string GetContentTypeFromPath(string path)
{
string extension = Path.GetExtension(path).ToLowerInvariant();
return extension switch
{
".jpg" or ".jpeg" => "image/jpeg",