FileStorageService.cs 5.46 KB
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Rcs.Application.Services;

namespace Rcs.Infrastructure.Services
{
    /// <summary>
    /// 文件存储服务实现
    /// </summary>
    public class FileStorageService : IFileStorageService
    {
        private readonly ILogger<FileStorageService> _logger;
        private readonly string _rootPath;

        public FileStorageService(ILogger<FileStorageService> logger, IWebHostEnvironment environment)
        {
            _logger = logger;
            // 使用项目内容根目录,确保 Debug 和 Release 模式下都指向项目文件夹
            _rootPath = Path.Combine(environment.ContentRootPath, "uploads");
            
            // 确保根目录存在
            if (!Directory.Exists(_rootPath))
            {
                Directory.CreateDirectory(_rootPath);
            }
            
            _logger.LogInformation("文件存储根路径: {RootPath}", _rootPath);
        }

        public async Task<FileStorageResult> SaveFileAsync(Stream stream, string fileName, string? subDirectory = null, CancellationToken cancellationToken = default)
        {
            if (stream == null || stream.Length == 0)
            {
                throw new ArgumentException("文件流不能为空");
            }

            if (string.IsNullOrWhiteSpace(fileName))
            {
                throw new ArgumentException("文件名不能为空");
            }

            try
            {
                // 生成唯一文件名
                var extension = Path.GetExtension(fileName);
                var uniqueFileName = $"{Guid.NewGuid()}{extension}";
                
                // 构建保存路径
                var savePath = _rootPath;
                if (!string.IsNullOrWhiteSpace(subDirectory))
                {
                    savePath = Path.Combine(_rootPath, subDirectory);
                    if (!Directory.Exists(savePath))
                    {
                        Directory.CreateDirectory(savePath);
                    }
                }

                var fullPath = Path.Combine(savePath, uniqueFileName);
                var relativePath = string.IsNullOrWhiteSpace(subDirectory) 
                    ? uniqueFileName 
                    : Path.Combine(subDirectory, uniqueFileName).Replace("\\", "/"); // 统一使用 URL 路径分隔符

                // 保存文件
                using (var fileStream = new FileStream(fullPath, FileMode.Create))
                {
                    stream.Position = 0; // 确保从头开始读取
                    await stream.CopyToAsync(fileStream, cancellationToken);
                }

                _logger.LogInformation("文件保存成功: {FilePath}", fullPath);

                return new FileStorageResult
                {
                    FileName = uniqueFileName,
                    OriginalFileName = fileName,
                    RelativePath = relativePath,
                    FullPath = fullPath,
                    FileSize = stream.Length,
                    Extension = extension
                };
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "保存文件失败: {FileName}", fileName);
                throw;
            }
        }

        public Task<bool> DeleteFileAsync(string filePath, CancellationToken cancellationToken = default)
        {
            try
            {
                var fullPath = Path.IsPathRooted(filePath) 
                    ? filePath 
                    : Path.Combine(_rootPath, filePath);

                if (File.Exists(fullPath))
                {
                    File.Delete(fullPath);
                    _logger.LogInformation("文件删除成功: {FilePath}", fullPath);
                    return Task.FromResult(true);
                }

                _logger.LogWarning("文件不存在: {FilePath}", fullPath);
                return Task.FromResult(false);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "删除文件失败: {FilePath}", filePath);
                return Task.FromResult(false);
            }
        }

        public string GetFullPath(string relativePath)
        {
            return Path.Combine(_rootPath, relativePath);
        }

        public (bool IsValid, string ErrorMessage) ValidateFile(string fileName, long fileSize, string[]? allowedExtensions = null, long? maxSizeInBytes = null)
        {
            if (string.IsNullOrWhiteSpace(fileName))
            {
                return (false, "文件名不能为空");
            }

            if (fileSize <= 0)
            {
                return (false, "文件不能为空");
            }

            // 验证文件大小
            if (maxSizeInBytes.HasValue && fileSize > maxSizeInBytes.Value)
            {
                var maxSizeMB = maxSizeInBytes.Value / 1024.0 / 1024.0;
                return (false, $"文件大小超过限制(最大 {maxSizeMB:F2} MB)");
            }

            // 验证文件扩展名
            if (allowedExtensions != null && allowedExtensions.Length > 0)
            {
                var extension = Path.GetExtension(fileName).ToLowerInvariant();
                if (!allowedExtensions.Contains(extension))
                {
                    return (false, $"不支持的文件类型,允许的类型: {string.Join(", ", allowedExtensions)}");
                }
            }

            return (true, string.Empty);
        }
    }
}