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:
193
Api/Management/StorageManagementController.cs
Normal file
193
Api/Management/StorageManagementController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user