319 lines
13 KiB
C#
319 lines
13 KiB
C#
|
|
using Microsoft.AspNetCore.Http;
|
|||
|
|
using Microsoft.Extensions.Configuration;
|
|||
|
|
using FutureMailAPI.DTOs;
|
|||
|
|
using System.Security.Cryptography;
|
|||
|
|
using System.Text;
|
|||
|
|
|
|||
|
|
namespace FutureMailAPI.Services
|
|||
|
|
{
|
|||
|
|
public class FileUploadService : IFileUploadService
|
|||
|
|
{
|
|||
|
|
private readonly IConfiguration _configuration;
|
|||
|
|
private readonly ILogger<FileUploadService> _logger;
|
|||
|
|
private readonly string _uploadPath;
|
|||
|
|
private readonly string _baseUrl;
|
|||
|
|
private readonly long _maxFileSize;
|
|||
|
|
private readonly List<string> _allowedImageTypes;
|
|||
|
|
private readonly List<string> _allowedVideoTypes;
|
|||
|
|
private readonly List<string> _allowedDocumentTypes;
|
|||
|
|
private readonly List<string> _allowedAudioTypes;
|
|||
|
|
|
|||
|
|
public FileUploadService(IConfiguration configuration, ILogger<FileUploadService> logger)
|
|||
|
|
{
|
|||
|
|
_configuration = configuration;
|
|||
|
|
_logger = logger;
|
|||
|
|
|
|||
|
|
// 从配置中获取上传路径和基础URL
|
|||
|
|
_uploadPath = configuration["FileUpload:UploadPath"] ?? "uploads";
|
|||
|
|
_baseUrl = configuration["FileUpload:BaseUrl"] ?? "http://localhost:5054/uploads";
|
|||
|
|
_maxFileSize = long.Parse(configuration["FileUpload:MaxFileSize"] ?? "104857600"); // 默认100MB
|
|||
|
|
|
|||
|
|
// 允许的文件类型
|
|||
|
|
_allowedImageTypes = new List<string> { "image/jpeg", "image/png", "image/gif", "image/webp" };
|
|||
|
|
_allowedVideoTypes = new List<string> { "video/mp4", "video/avi", "video/mov", "video/wmv" };
|
|||
|
|
_allowedDocumentTypes = new List<string> { "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" };
|
|||
|
|
_allowedAudioTypes = new List<string> { "audio/mpeg", "audio/wav", "audio/ogg" };
|
|||
|
|
|
|||
|
|
// 确保上传目录存在
|
|||
|
|
EnsureUploadDirectoryExists();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task<ApiResponse<FileUploadResponseDto>> UploadFileAsync(IFormFile file, int userId, FileUploadRequestDto request)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (file == null || file.Length == 0)
|
|||
|
|
{
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.ErrorResult("请选择要上传的文件");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查文件大小
|
|||
|
|
if (file.Length > _maxFileSize)
|
|||
|
|
{
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.ErrorResult($"文件大小不能超过 {_maxFileSize / (1024 * 1024)}MB");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查文件类型
|
|||
|
|
var contentType = file.ContentType.ToLower();
|
|||
|
|
if (!IsAllowedFileType(contentType, request.Type))
|
|||
|
|
{
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.ErrorResult("不支持的文件类型");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成唯一文件名
|
|||
|
|
var fileId = GenerateFileId();
|
|||
|
|
var fileExtension = Path.GetExtension(file.FileName);
|
|||
|
|
var fileName = $"{fileId}{fileExtension}";
|
|||
|
|
|
|||
|
|
// 创建用户目录
|
|||
|
|
var userDirectory = Path.Combine(_uploadPath, userId.ToString());
|
|||
|
|
var categoryDirectory = string.IsNullOrEmpty(request.Category)
|
|||
|
|
? userDirectory
|
|||
|
|
: Path.Combine(userDirectory, request.Category);
|
|||
|
|
|
|||
|
|
Directory.CreateDirectory(categoryDirectory);
|
|||
|
|
|
|||
|
|
// 保存文件
|
|||
|
|
var filePath = Path.Combine(categoryDirectory, fileName);
|
|||
|
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
|||
|
|
{
|
|||
|
|
await file.CopyToAsync(stream);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成缩略图(如果是图片)
|
|||
|
|
string thumbnailUrl = string.Empty;
|
|||
|
|
if (request.Type == AttachmentType.IMAGE)
|
|||
|
|
{
|
|||
|
|
thumbnailUrl = await GenerateThumbnailAsync(filePath, fileName, categoryDirectory);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建文件URL
|
|||
|
|
var relativePath = Path.GetRelativePath(_uploadPath, filePath).Replace("\\", "/");
|
|||
|
|
var fileUrl = $"{_baseUrl}/{relativePath}";
|
|||
|
|
|
|||
|
|
// 构建缩略图URL
|
|||
|
|
if (!string.IsNullOrEmpty(thumbnailUrl))
|
|||
|
|
{
|
|||
|
|
var relativeThumbnailPath = Path.GetRelativePath(_uploadPath, thumbnailUrl).Replace("\\", "/");
|
|||
|
|
thumbnailUrl = $"{_baseUrl}/{relativeThumbnailPath}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var response = new FileUploadResponseDto
|
|||
|
|
{
|
|||
|
|
FileId = fileId,
|
|||
|
|
FileName = file.FileName,
|
|||
|
|
FileUrl = fileUrl,
|
|||
|
|
ThumbnailUrl = thumbnailUrl,
|
|||
|
|
FileSize = file.Length,
|
|||
|
|
ContentType = file.ContentType,
|
|||
|
|
Type = request.Type,
|
|||
|
|
UploadedAt = DateTime.UtcNow
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.SuccessResult(response);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "上传文件时发生错误");
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.ErrorResult("上传文件失败");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task<ApiResponse<bool>> DeleteFileAsync(string fileId, int userId)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 在实际应用中,这里应该从数据库中查找文件信息
|
|||
|
|
// 目前我们只是简单地根据文件ID删除文件
|
|||
|
|
|
|||
|
|
// 查找用户目录下的所有文件
|
|||
|
|
var userDirectory = Path.Combine(_uploadPath, userId.ToString());
|
|||
|
|
if (!Directory.Exists(userDirectory))
|
|||
|
|
{
|
|||
|
|
return ApiResponse<bool>.ErrorResult("文件不存在");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查找匹配的文件
|
|||
|
|
var files = Directory.GetFiles(userDirectory, $"{fileId}*", SearchOption.AllDirectories);
|
|||
|
|
if (files.Length == 0)
|
|||
|
|
{
|
|||
|
|
return ApiResponse<bool>.ErrorResult("文件不存在");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除所有相关文件(包括缩略图)
|
|||
|
|
foreach (var file in files)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
System.IO.File.Delete(file);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogWarning(ex, $"删除文件 {file} 失败");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ApiResponse<bool>.SuccessResult(true);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "删除文件时发生错误");
|
|||
|
|
return ApiResponse<bool>.ErrorResult("删除文件失败");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task<ApiResponse<FileUploadResponseDto>> GetFileAsync(string fileId, int userId)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 在实际应用中,这里应该从数据库中查找文件信息
|
|||
|
|
// 目前我们只是简单地根据文件ID返回文件信息
|
|||
|
|
|
|||
|
|
// 查找用户目录下的所有文件
|
|||
|
|
var userDirectory = Path.Combine(_uploadPath, userId.ToString());
|
|||
|
|
if (!Directory.Exists(userDirectory))
|
|||
|
|
{
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.ErrorResult("文件不存在");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查找匹配的文件
|
|||
|
|
var files = Directory.GetFiles(userDirectory, $"{fileId}*", SearchOption.AllDirectories);
|
|||
|
|
if (files.Length == 0)
|
|||
|
|
{
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.ErrorResult("文件不存在");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取主文件(排除缩略图)
|
|||
|
|
var mainFile = files.FirstOrDefault(f => !f.Contains("_thumb."));
|
|||
|
|
if (mainFile == null)
|
|||
|
|
{
|
|||
|
|
mainFile = files[0]; // 如果没有找到主文件,使用第一个文件
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var fileInfo = new FileInfo(mainFile);
|
|||
|
|
var relativePath = Path.GetRelativePath(_uploadPath, mainFile).Replace("\\", "/");
|
|||
|
|
var fileUrl = $"{_baseUrl}/{relativePath}";
|
|||
|
|
|
|||
|
|
// 查找缩略图
|
|||
|
|
var thumbnailFile = files.FirstOrDefault(f => f.Contains("_thumb."));
|
|||
|
|
string thumbnailUrl = string.Empty;
|
|||
|
|
if (thumbnailFile != null)
|
|||
|
|
{
|
|||
|
|
var relativeThumbnailPath = Path.GetRelativePath(_uploadPath, thumbnailFile).Replace("\\", "/");
|
|||
|
|
thumbnailUrl = $"{_baseUrl}/{relativeThumbnailPath}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确定文件类型
|
|||
|
|
var contentType = GetContentType(fileInfo.Extension);
|
|||
|
|
var attachmentType = GetAttachmentType(contentType);
|
|||
|
|
|
|||
|
|
var response = new FileUploadResponseDto
|
|||
|
|
{
|
|||
|
|
FileId = fileId,
|
|||
|
|
FileName = fileInfo.Name,
|
|||
|
|
FileUrl = fileUrl,
|
|||
|
|
ThumbnailUrl = thumbnailUrl,
|
|||
|
|
FileSize = fileInfo.Length,
|
|||
|
|
ContentType = contentType,
|
|||
|
|
Type = attachmentType,
|
|||
|
|
UploadedAt = fileInfo.CreationTimeUtc
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.SuccessResult(response);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "获取文件信息时发生错误");
|
|||
|
|
return ApiResponse<FileUploadResponseDto>.ErrorResult("获取文件信息失败");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void EnsureUploadDirectoryExists()
|
|||
|
|
{
|
|||
|
|
if (!Directory.Exists(_uploadPath))
|
|||
|
|
{
|
|||
|
|
Directory.CreateDirectory(_uploadPath);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private bool IsAllowedFileType(string contentType, AttachmentType type)
|
|||
|
|
{
|
|||
|
|
return type switch
|
|||
|
|
{
|
|||
|
|
AttachmentType.IMAGE => _allowedImageTypes.Contains(contentType),
|
|||
|
|
AttachmentType.VIDEO => _allowedVideoTypes.Contains(contentType),
|
|||
|
|
AttachmentType.DOCUMENT => _allowedDocumentTypes.Contains(contentType),
|
|||
|
|
AttachmentType.VOICE => _allowedAudioTypes.Contains(contentType),
|
|||
|
|
_ => true // 其他类型暂时允许
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private string GenerateFileId()
|
|||
|
|
{
|
|||
|
|
return Guid.NewGuid().ToString("N");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async Task<string> GenerateThumbnailAsync(string filePath, string fileName, string directory)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 在实际应用中,这里应该使用图像处理库(如ImageSharp)生成缩略图
|
|||
|
|
// 目前我们只是创建一个简单的缩略图文件名
|
|||
|
|
|
|||
|
|
var fileExtension = Path.GetExtension(fileName);
|
|||
|
|
var thumbnailFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_thumb{fileExtension}";
|
|||
|
|
var thumbnailPath = Path.Combine(directory, thumbnailFileName);
|
|||
|
|
|
|||
|
|
// 这里应该添加实际的缩略图生成代码
|
|||
|
|
// 暂时只是复制原文件作为缩略图
|
|||
|
|
System.IO.File.Copy(filePath, thumbnailPath);
|
|||
|
|
|
|||
|
|
return thumbnailPath;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogWarning(ex, "生成缩略图失败");
|
|||
|
|
return string.Empty;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private string GetContentType(string extension)
|
|||
|
|
{
|
|||
|
|
return extension.ToLower() switch
|
|||
|
|
{
|
|||
|
|
".jpg" or ".jpeg" => "image/jpeg",
|
|||
|
|
".png" => "image/png",
|
|||
|
|
".gif" => "image/gif",
|
|||
|
|
".webp" => "image/webp",
|
|||
|
|
".mp4" => "video/mp4",
|
|||
|
|
".avi" => "video/avi",
|
|||
|
|
".mov" => "video/mov",
|
|||
|
|
".wmv" => "video/wmv",
|
|||
|
|
".mp3" => "audio/mpeg",
|
|||
|
|
".wav" => "audio/wav",
|
|||
|
|
".ogg" => "audio/ogg",
|
|||
|
|
".pdf" => "application/pdf",
|
|||
|
|
".doc" => "application/msword",
|
|||
|
|
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|||
|
|
_ => "application/octet-stream"
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private AttachmentType GetAttachmentType(string contentType)
|
|||
|
|
{
|
|||
|
|
if (_allowedImageTypes.Contains(contentType))
|
|||
|
|
return AttachmentType.IMAGE;
|
|||
|
|
|
|||
|
|
if (_allowedVideoTypes.Contains(contentType))
|
|||
|
|
return AttachmentType.VIDEO;
|
|||
|
|
|
|||
|
|
if (_allowedAudioTypes.Contains(contentType))
|
|||
|
|
return AttachmentType.VOICE;
|
|||
|
|
|
|||
|
|
if (_allowedDocumentTypes.Contains(contentType))
|
|||
|
|
return AttachmentType.DOCUMENT;
|
|||
|
|
|
|||
|
|
return AttachmentType.OTHER;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|