C# 系列教程导航
本文属于《C# 实战开发系列》第 17 篇
| 01 | C# 基础语法速成 | 变量、流程控制、异常处理 |
| 02 | C# 面向对象编程 | 类、继承、多态、接口 |
| 03 | C# 泛型与集合 | List、Dictionary、泛型约束 |
| 04 | C# LINQ 实战 | 查询表达式、延迟执行 |
| 05 | C# 异步编程 | async/await、Task并行 |
| 06 | C# 文件与IO操作 | File、Stream、序列化 |
| 07 | C# 多线程编程 | Thread、ThreadPool、锁 |
| 08 | C# 网络编程基础 | HttpClient、Socket |
| 09 | C# 正则表达式实战 | 模式匹配、文本处理 |
| 10 | C# 反射与特性 | 动态调用、自定义特性 |
| 11 | C# 依赖注入实战 | DI容器、服务生命周期 |
| 12 | C# 单元测试实战 | xUnit、Moq、测试驱动 |
| 13 | C# 日志与监控 | Serilog、ELK集成 |
| 14 | C# 配置管理 | appsettings、环境变量 |
| 15 | C# 缓存策略实战 | MemoryCache、Redis |
| 16 | C# 数据库访问 | ADO.NET、Dapper、EF Core |
| 17 | C# 文件上传下载实战 | 本篇:本地存储与云存储 |
| 18 | C# Excel 导入导出实战 | EPPlus、NPOI、大数据处理 |
| 19 | C# PDF 生成与解析 | iTextSharp、PdfSharp |
| 20 | C# 图像处理实战 | SkiaSharp、缩略图、水印 |
C# 文件上传下载实战:从本地存储到云存储,完整解决方案
前言
在现代 Web 应用开发中,文件上传下载是最常见也是最容易出现问题的功能之一。无论是用户头像上传、文档管理系统,还是大型企业的文件存储服务,都需要处理各种复杂的场景:
- 单文件上传:基础但需要考虑安全校验
- 多文件上传:批量处理与进度反馈
- 大文件上传:分片上传、断点续传
- 文件下载:断点续传、限速下载
- 安全校验:文件类型、大小限制、病毒扫描
- 云存储集成:阿里云 OSS、Azure Blob、AWS S3
本文将从零开始,通过大量实战代码,带你构建一个完整的文件上传下载解决方案。
第一部分:单文件上传
1.1 基础控制器实现
首先创建一个基础的文件上传控制器:
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
public class FileUploadController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
private readonly ILogger<FileUploadController> _logger;
private readonly string _uploadFolder;
public FileUploadController(
IWebHostEnvironment environment,
ILogger<FileUploadController> logger)
{
_environment = environment;
_logger = logger;
_uploadFolder = Path.Combine(environment.WebRootPath, "uploads");
// 确保上传目录存在
if (!Directory.Exists(_uploadFolder))
{
Directory.CreateDirectory(_uploadFolder);
}
}
/// <summary>
/// 单文件上传
/// </summary>
[HttpPost("upload")]
public async Task<IActionResult> UploadSingle(IFormFile file)
{
if (file == null || file.Length == 0)
{
return BadRequest(new { code = 400, message = "请选择要上传的文件" });
}
try
{
// 生成唯一文件名
var fileExtension = Path.GetExtension(file.FileName);
var fileName = $"{Guid.NewGuid()}{fileExtension}";
var filePath = Path.Combine(_uploadFolder, fileName);
// 保存文件
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
_logger.LogInformation($"文件上传成功: {file.FileName} -> {fileName}");
return Ok(new
{
code = 200,
message = "上传成功",
data = new
{
originalName = file.FileName,
fileName = fileName,
size = file.Length,
url = $"/uploads/{fileName}",
contentType = file.ContentType
}
});
}
catch (Exception ex)
{
_logger.LogError(ex, "文件上传失败");
return StatusCode(500, new { code = 500, message = "文件上传失败" });
}
}
}
1.2 文件大小与类型校验
添加文件校验逻辑:
public class FileValidationService
{
// 允许的文件类型
private static readonly Dictionary<string, List<string>> AllowedFileTypes = new()
{
["image"] = new List<string> { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" },
["document"] = new List<string> { ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx" },
["video"] = new List<string> { ".mp4", ".avi", ".mov", ".wmv", ".flv" },
["archive"] = new List<string> { ".zip", ".rar", ".7z", ".tar", ".gz" }
};
// 文件大小限制(字节)
private static readonly Dictionary<string, long> MaxFileSizes = new()
{
["image"] = 10 * 1024 * 1024, // 10MB
["document"] = 50 * 1024 * 1024, // 50MB
["video"] = 500 * 1024 * 1024, // 500MB
["archive"] = 100 * 1024 * 1024 // 100MB
};
/// <summary>
/// 校验文件
/// </summary>
public (bool IsValid, string ErrorMessage) ValidateFile(
IFormFile file,
string category = "image")
{
// 检查文件是否为空
if (file == null || file.Length == 0)
{
return (false, "文件不能为空");
}
// 获取文件扩展名
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
// 检查文件类型
if (!AllowedFileTypes.ContainsKey(category) ||
!AllowedFileTypes[category].Contains(extension))
{
var allowedTypes = string.Join(", ", AllowedFileTypes[category]);
return (false, $"不支持的文件类型,允许的类型: {allowedTypes}");
}
// 检查文件大小
if (file.Length > MaxFileSizes[category])
{
var maxSizeMB = MaxFileSizes[category] / (1024 * 1024);
return (false, $"文件大小超过限制,最大允许 {maxSizeMB}MB");
}
return (true, string.Empty);
}
/// <summary>
/// 通过文件头校验真实类型(防止扩展名伪造)
/// </summary>
public bool ValidateFileHeader(string filePath, string expectedType)
{
var fileHeaders = new Dictionary<string, byte[]>
{
["jpg"] = new byte[] { 0xFF, 0xD8, 0xFF },
["png"] = new byte[] { 0x89, 0x50, 0x4E, 0x47 },
["gif"] = new byte[] { 0x47, 0x49, 0x46, 0x38 },
["pdf"] = new byte[] { 0x25, 0x50, 0x44, 0x46 },
["zip"] = new byte[] { 0x50, 0x4B, 0x03, 0x04 }
};
if (!fileHeaders.ContainsKey(expectedType))
return true; // 未配置的类型跳过校验
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var header = new byte[fileHeaders[expectedType].Length];
fs.Read(header, 0, header.Length);
return header.SequenceEqual(fileHeaders[expectedType]);
}
}
1.3 完善的上传接口
整合校验逻辑的完整实现:
[HttpPost("upload-with-validation")]
public async Task<IActionResult> UploadWithValidation(
IFormFile file,
[FromQuery] string category = "image")
{
// 文件校验
var validation = _validationService.ValidateFile(file, category);
if (!validation.IsValid)
{
return BadRequest(new { code = 400, message = validation.ErrorMessage });
}
try
{
// 按日期分目录存储
var dateFolder = DateTime.Now.ToString("yyyy/MM/dd");
var saveFolder = Path.Combine(_uploadFolder, category, dateFolder);
Directory.CreateDirectory(saveFolder);
// 生成文件名
var extension = Path.GetExtension(file.FileName);
var fileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(saveFolder, fileName);
// 保存文件
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
// 文件头校验
var typeWithoutDot = extension.TrimStart('.');
if (!_validationService.ValidateFileHeader(filePath, typeWithoutDot))
{
System.IO.File.Delete(filePath);
return BadRequest(new { code = 400, message = "文件内容与类型不匹配" });
}
// 返回结果
var relativePath = $"/uploads/{category}/{dateFolder}/{fileName}";
return Ok(new
{
code = 200,
message = "上传成功",
data = new FileUploadResult
{
OriginalName = file.FileName,
FileName = fileName,
Size = file.Length,
Url = relativePath,
ContentType = file.ContentType,
UploadTime = DateTime.Now
}
});
}
catch (Exception ex)
{
_logger.LogError(ex, "文件上传异常");
return StatusCode(500, new { code = 500, message = "服务器内部错误" });
}
}
public class FileUploadResult
{
public string OriginalName { get; set; }
public string FileName { get; set; }
public long Size { get; set; }
public string Url { get; set; }
public string ContentType { get; set; }
public DateTime UploadTime { get; set; }
}
第二部分:多文件上传
2.1 批量上传实现
/// <summary>
/// 多文件上传
/// </summary>
[HttpPost("upload-multiple")]
public async Task<IActionResult> UploadMultiple(
List<IFormFile> files,
[FromQuery] string category = "image")
{
if (files == null || files.Count == 0)
{
return BadRequest(new { code = 400, message = "请选择要上传的文件" });
}
// 限制单次上传数量
const int maxFileCount = 10;
if (files.Count > maxFileCount)
{
return BadRequest(new { code = 400, message = $"单次最多上传 {maxFileCount} 个文件" });
}
var results = new List<FileUploadResult>();
var errors = new List<string>();
foreach (var file in files)
{
// 校验单个文件
var validation = _validationService.ValidateFile(file, category);
if (!validation.IsValid)
{
errors.Add($"{file.FileName}: {validation.ErrorMessage}");
continue;
}
try
{
var result = await SaveFileAsync(file, category);
results.Add(result);
}
catch (Exception ex)
{
_logger.LogError(ex, $"文件保存失败: {file.FileName}");
errors.Add($"{file.FileName}: 保存失败");
}
}
return Ok(new
{
code = 200,
message = $"成功上传 {results.Count} 个文件",
data = new
{
successCount = results.Count,
failCount = errors.Count,
files = results,
errors = errors
}
});
}
/// <summary>
/// 保存单个文件(私有方法)
/// </summary>
private async Task<FileUploadResult> SaveFileAsync(IFormFile file, string category)
{
var dateFolder = DateTime.Now.ToString("yyyy/MM/dd");
var saveFolder = Path.Combine(_uploadFolder, category, dateFolder);
Directory.CreateDirectory(saveFolder);
var extension = Path.GetExtension(file.FileName);
var fileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(saveFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return new FileUploadResult
{
OriginalName = file.FileName,
FileName = fileName,
Size = file.Length,
Url = $"/uploads/{category}/{dateFolder}/{fileName}",
ContentType = file.ContentType,
UploadTime = DateTime.Now
};
}
2.2 带进度的多文件上传
前端配合实现上传进度:
/// <summary>
/// 获取上传进度(配合前端轮询或 SignalR)
/// </summary>
public class UploadProgressService
{
private static readonly Dictionary<string, UploadProgress> _progressCache = new();
private static readonly object _lock = new();
public string StartUpload(int totalFiles)
{
var uploadId = Guid.NewGuid().ToString("N");
lock (_lock)
{
_progressCache[uploadId] = new UploadProgress
{
UploadId = uploadId,
TotalFiles = totalFiles,
CompletedFiles = 0,
CurrentFile = string.Empty,
Status = "uploading",
StartTime = DateTime.Now
};
}
return uploadId;
}
public void UpdateProgress(string uploadId, string currentFile, int completed)
{
lock (_lock)
{
if (_progressCache.TryGetValue(uploadId, out var progress))
{
progress.CurrentFile = currentFile;
progress.CompletedFiles = completed;
progress.Progress = (int)((double)completed / progress.TotalFiles * 100);
if (completed == progress.TotalFiles)
{
progress.Status = "completed";
progress.EndTime = DateTime.Now;
}
}
}
}
public UploadProgress GetProgress(string uploadId)
{
lock (_lock)
{
return _progressCache.TryGetValue(uploadId, out var progress)
? progress
: null;
}
}
}
public class UploadProgress
{
public string UploadId { get; set; }
public int TotalFiles { get; set; }
public int CompletedFiles { get; set; }
public int Progress { get; set; }
public string CurrentFile { get; set; }
public string Status { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
}
2.3 控制器整合进度追踪
[HttpPost("upload-with-progress")]
public async Task<IActionResult> UploadWithProgress(
List<IFormFile> files,
[FromQuery] string category = "image")
{
var uploadId = _progressService.StartUpload(files.Count);
var results = new List<FileUploadResult>();
for (int i = 0; i < files.Count; i++)
{
var file = files[i];
_progressService.UpdateProgress(uploadId, file.FileName, i);
try
{
var result = await SaveFileAsync(file, category);
results.Add(result);
}
catch (Exception ex)
{
_logger.LogError(ex, $"上传失败: {file.FileName}");
}
}
_progressService.UpdateProgress(uploadId, string.Empty, files.Count);
return Ok(new
{
code = 200,
uploadId = uploadId,
files = results
});
}
[HttpGet("progress/{uploadId}")]
public IActionResult GetProgress(string uploadId)
{
var progress = _progressService.GetProgress(uploadId);
if (progress == null)
{
return NotFound(new { code = 404, message = "上传任务不存在" });
}
return Ok(new { code = 200, data = progress });
}
第三部分:大文件分片上传
3.1 分片上传模型
/// <summary>
/// 分片上传请求模型
/// </summary>
public class ChunkUploadRequest
{
/// <summary>
/// 文件唯一标识(前端生成)
/// </summary>
public string FileId { get; set; }
/// <summary>
/// 当前分片索引(从0开始)
/// </summary>
public int ChunkIndex { get; set; }
/// <summary>
/// 总分片数
/// </summary>
public int TotalChunks { get; set; }
/// <summary>
/// 原始文件名
/// </summary>
public string FileName { get; set; }
/// <summary>
/// 文件总大小
/// </summary>
public long TotalSize { get; set; }
/// <summary>
/// 分片文件
/// </summary>
public IFormFile Chunk { get; set; }
}
/// <summary>
/// 分片信息
/// </summary>
public class ChunkInfo
{
public string FileId { get; set; }
public string FileName { get; set; }
public long TotalSize { get; set; }
public int TotalChunks { get; set; }
public HashSet<int> UploadedChunks { get; set; } = new();
public string TempFolder { get; set; }
public DateTime CreateTime { get; set; }
}
3.2 分片上传服务
public class ChunkUploadService
{
private readonly IWebHostEnvironment _environment;
private readonly ILogger<ChunkUploadService> _logger;
private readonly string _tempFolder;
private readonly string _uploadFolder;
// 分片信息缓存(生产环境应使用 Redis)
private static readonly Dictionary<string, ChunkInfo> _chunkCache = new();
private static readonly object _lock = new();
public ChunkUploadService(IWebHostEnvironment environment, ILogger<ChunkUploadService> logger)
{
_environment = environment;
_logger = logger;
_tempFolder = Path.Combine(environment.WebRootPath, "temp", "chunks");
_uploadFolder = Path.Combine(environment.WebRootPath, "uploads");
Directory.CreateDirectory(_tempFolder);
Directory.CreateDirectory(_uploadFolder);
}
/// <summary>
/// 上传分片
/// </summary>
public async Task<ChunkUploadResult> UploadChunkAsync(ChunkUploadRequest request)
{
lock (_lock)
{
// 初始化或获取分片信息
if (!_chunkCache.ContainsKey(request.FileId))
{
var tempPath = Path.Combine(_tempFolder, request.FileId);
Directory.CreateDirectory(tempPath);
_chunkCache[request.FileId] = new ChunkInfo
{
FileId = request.FileId,
FileName = request.FileName,
TotalSize = request.TotalSize,
TotalChunks = request.TotalChunks,
TempFolder = tempPath,
CreateTime = DateTime.Now
};
}
}
var chunkInfo = _chunkCache[request.FileId];
// 检查分片是否已上传(断点续传)
if (chunkInfo.UploadedChunks.Contains(request.ChunkIndex))
{
return new ChunkUploadResult
{
Status = "duplicate",
Message = "分片已存在",
UploadedChunks = chunkInfo.UploadedChunks.Count,
TotalChunks = chunkInfo.TotalChunks
};
}
// 保存分片
var chunkPath = Path.Combine(chunkInfo.TempFolder, $"chunk_{request.ChunkIndex}");
using (var stream = new FileStream(chunkPath, FileMode.Create))
{
await request.Chunk.CopyToAsync(stream);
}
// 更新已上传分片列表
lock (_lock)
{
chunkInfo.UploadedChunks.Add(request.ChunkIndex);
}
_logger.LogInformation($"分片上传成功: {request.FileId} – {request.ChunkIndex + 1}/{request.TotalChunks}");
return new ChunkUploadResult
{
Status = "success",
Message = "分片上传成功",
UploadedChunks = chunkInfo.UploadedChunks.Count,
TotalChunks = chunkInfo.TotalChunks
};
}
/// <summary>
/// 合并分片
/// </summary>
public async Task<MergeResult> MergeChunksAsync(string fileId)
{
if (!_chunkCache.TryGetValue(fileId, out var chunkInfo))
{
return new MergeResult { Success = false, Message = "文件信息不存在" };
}
// 检查是否所有分片都已上传
if (chunkInfo.UploadedChunks.Count != chunkInfo.TotalChunks)
{
return new MergeResult
{
Success = false,
Message = $"分片不完整,已上传 {chunkInfo.UploadedChunks.Count}/{chunkInfo.TotalChunks}"
};
}
try
{
// 生成最终文件路径
var extension = Path.GetExtension(chunkInfo.FileName);
var finalFileName = $"{Guid.NewGuid()}{extension}";
var dateFolder = DateTime.Now.ToString("yyyy/MM/dd");
var finalFolder = Path.Combine(_uploadFolder, dateFolder);
Directory.CreateDirectory(finalFolder);
var finalPath = Path.Combine(finalFolder, finalFileName);
// 合并分片
await MergeFilesAsync(chunkInfo.TempFolder, finalPath, chunkInfo.TotalChunks);
// 清理临时文件
Directory.Delete(chunkInfo.TempFolder, true);
_chunkCache.Remove(fileId);
_logger.LogInformation($"文件合并成功: {chunkInfo.FileName} -> {finalFileName}");
return new MergeResult
{
Success = true,
Message = "文件合并成功",
FileName = finalFileName,
OriginalName = chunkInfo.FileName,
Url = $"/uploads/{dateFolder}/{finalFileName}",
Size = chunkInfo.TotalSize
};
}
catch (Exception ex)
{
_logger.LogError(ex, "文件合并失败");
return new MergeResult { Success = false, Message = "文件合并失败" };
}
}
/// <summary>
/// 合并文件
/// </summary>
private async Task MergeFilesAsync(string tempFolder, string finalPath, int totalChunks)
{
await using var finalStream = new FileStream(finalPath, FileMode.Create);
for (int i = 0; i < totalChunks; i++)
{
var chunkPath = Path.Combine(tempFolder, $"chunk_{i}");
using var chunkStream = new FileStream(chunkPath, FileMode.Open);
await chunkStream.CopyToAsync(finalStream);
}
}
/// <summary>
/// 获取已上传的分片列表(用于断点续传)
/// </summary>
public List<int> GetUploadedChunks(string fileId)
{
if (_chunkCache.TryGetValue(fileId, out var chunkInfo))
{
return chunkInfo.UploadedChunks.ToList();
}
return new List<int>();
}
}
public class ChunkUploadResult
{
public string Status { get; set; }
public string Message { get; set; }
public int UploadedChunks { get; set; }
public int TotalChunks { get; set; }
}
public class MergeResult
{
public bool Success { get; set; }
public string Message { get; set; }
public string FileName { get; set; }
public string OriginalName { get; set; }
public string Url { get; set; }
public long Size { get; set; }
}
3.3 分片上传控制器
[ApiController]
[Route("api/[controller]")]
public class ChunkUploadController : ControllerBase
{
private readonly ChunkUploadService _chunkService;
private readonly ILogger<ChunkUploadController> _logger;
public ChunkUploadController(ChunkUploadService chunkService, ILogger<ChunkUploadController> logger)
{
_chunkService = chunkService;
_logger = logger;
}
/// <summary>
/// 上传分片
/// </summary>
[HttpPost("chunk")]
public async Task<IActionResult> UploadChunk([FromForm] ChunkUploadRequest request)
{
if (request.Chunk == null || request.Chunk.Length == 0)
{
return BadRequest(new { code = 400, message = "分片数据为空" });
}
var result = await _chunkService.UploadChunkAsync(request);
return Ok(new { code = 200, data = result });
}
/// <summary>
/// 合并分片
/// </summary>
[HttpPost("merge")]
public async Task<IActionResult> MergeChunks([FromBody] MergeRequest request)
{
var result = await _chunkService.MergeChunksAsync(request.FileId);
if (!result.Success)
{
return BadRequest(new { code = 400, message = result.Message });
}
return Ok(new { code = 200, message = result.Message, data = result });
}
/// <summary>
/// 获取已上传分片(断点续传)
/// </summary>
[HttpGet("uploaded-chunks/{fileId}")]
public IActionResult GetUploadedChunks(string fileId)
{
var chunks = _chunkService.GetUploadedChunks(fileId);
return Ok(new { code = 200, data = chunks });
}
}
public class MergeRequest
{
public string FileId { get; set; }
}
第四部分:文件下载与断点续传
4.1 基础文件下载
/// <summary>
/// 基础文件下载
/// </summary>
[HttpGet("download")]
public IActionResult Download([FromQuery] string fileName)
{
var filePath = Path.Combine(_uploadFolder, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound(new { code = 404, message = "文件不存在" });
}
var fileBytes = System.IO.File.ReadAllBytes(filePath);
var contentType = GetContentType(fileName);
return File(fileBytes, contentType, fileName);
}
/// <summary>
/// 获取文件 MIME 类型
/// </summary>
private string GetContentType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".pdf" => "application/pdf",
".doc" => "application/msword",
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls" => "application/vnd.ms-excel",
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".zip" => "application/zip",
".txt" => "text/plain",
_ => "application/octet-stream"
};
}
4.2 断点续传下载
/// <summary>
/// 支持断点续传的文件下载
/// </summary>
[HttpGet("download-resumable")]
public IActionResult DownloadResumable([FromQuery] string fileName)
{
var filePath = Path.Combine(_uploadFolder, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound(new { code = 404, message = "文件不存在" });
}
var fileInfo = new FileInfo(filePath);
var fileLength = fileInfo.Length;
var contentType = GetContentType(fileName);
// 获取 Range 请求头
var rangeHeader = Request.Headers["Range"].FirstOrDefault();
if (string.IsNullOrEmpty(rangeHeader))
{
// 无 Range 头,返回完整文件
return PhysicalFile(filePath, contentType, fileName);
}
// 解析 Range 头(格式:bytes=start-end)
var range = ParseRangeHeader(rangeHeader, fileLength);
if (range == null)
{
return StatusCode(416); // Range Not Satisfiable
}
var (start, end) = range.Value;
var contentLength = end – start + 1;
// 设置响应头
Response.StatusCode = 206; // Partial Content
Response.Headers.Add("Content-Range", $"bytes {start}–{end}/{fileLength}");
Response.Headers.Add("Content-Length", contentLength.ToString());
Response.Headers.Add("Accept-Ranges", "bytes");
// 流式传输指定范围
return new FileStreamResult(
new PartialFileStream(filePath, start, contentLength),
contentType
);
}
/// <summary>
/// 解析 Range 请求头
/// </summary>
private (long Start, long End)? ParseRangeHeader(string rangeHeader, long fileLength)
{
// 格式:bytes=start-end 或 bytes=start-
if (!rangeHeader.StartsWith("bytes="))
return null;
var rangeSpec = rangeHeader.Substring(6);
var parts = rangeSpec.Split('-');
if (parts.Length != 2)
return null;
long start, end;
if (!long.TryParse(parts[0], out start))
return null;
if (string.IsNullOrEmpty(parts[1]) || !long.TryParse(parts[1], out end))
end = fileLength – 1;
// 校验范围有效性
if (start < 0 || start >= fileLength || end < start || end >= fileLength)
return null;
return (start, end);
}
/// <summary>
/// 部分文件流
/// </summary>
public class PartialFileStream : Stream
{
private readonly FileStream _fileStream;
private readonly long _start;
private readonly long _length;
private long _position;
public PartialFileStream(string filePath, long start, long length)
{
_fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
_fileStream.Seek(start, SeekOrigin.Begin);
_start = start;
_length = length;
_position = 0;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _length;
public override long Position
{
get => _position;
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
var remaining = _length – _position;
if (remaining <= 0) return 0;
var bytesToRead = (int)Math.Min(count, remaining);
var bytesRead = _fileStream.Read(buffer, offset, bytesToRead);
_position += bytesRead;
return bytesRead;
}
public override void Flush() => _fileStream.Flush();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
protected override void Dispose(bool disposing)
{
if (disposing) _fileStream.Dispose();
base.Dispose(disposing);
}
}
4.3 下载限速实现
/// <summary>
/// 限速下载服务
/// </summary>
public class ThrottledDownloadService
{
/// <summary>
/// 限速下载文件
/// </summary>
public async Task DownloadWithThrottleAsync(
string filePath,
Stream outputStream,
long maxBytesPerSecond,
CancellationToken cancellationToken = default)
{
var buffer = new byte[81920]; // 80KB 缓冲区
var maxBytesPerTick = maxBytesPerSecond / 10; // 每100ms的最大字节数
var tickInterval = TimeSpan.FromMilliseconds(100);
await using var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
buffer.Length,
FileOptions.SequentialScan);
int bytesRead;
var bytesThisTick = 0L;
var tickStart = DateTime.UtcNow;
while ((bytesRead = await fileStream.ReadAsync(buffer, cancellationToken)) > 0)
{
await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
bytesThisTick += bytesRead;
// 检查是否需要限速等待
if (bytesThisTick >= maxBytesPerTick)
{
var elapsed = DateTime.UtcNow – tickStart;
if (elapsed < tickInterval)
{
await Task.Delay(tickInterval – elapsed, cancellationToken);
}
bytesThisTick = 0;
tickStart = DateTime.UtcNow;
}
}
}
}
// 控制器中使用
[HttpGet("download-throttled")]
public async Task<IActionResult> DownloadThrottled(
[FromQuery] string fileName,
[FromQuery] long maxSpeed = 102400) // 默认 100KB/s
{
var filePath = Path.Combine(_uploadFolder, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound(new { code = 404, message = "文件不存在" });
}
var contentType = GetContentType(fileName);
Response.Headers.Add("Content-Disposition", $"attachment; filename=\\"{fileName}\\"");
Response.Headers.Add("Content-Type", contentType);
await _throttledDownloadService.DownloadWithThrottleAsync(
filePath,
Response.Body,
maxSpeed);
return new EmptyResult();
}
第五部分:文件安全校验
5.1 完整的安全校验服务
/// <summary>
/// 文件安全校验服务
/// </summary>
public class FileSecurityService
{
private readonly ILogger<FileSecurityService> _logger;
// 危险文件扩展名
private static readonly HashSet<string> DangerousExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".exe", ".bat", ".cmd", ".com", ".pif", ".scr", ".vbs", ".js",
".jar", ".msi", ".dll", ".sh", ".php", ".asp", ".aspx", ".jsp"
};
// 文件魔数(文件头)签名
private static readonly Dictionary<string, byte[][]> FileSignatures = new()
{
[".jpg"] = new[] { new byte[] { 0xFF, 0xD8, 0xFF } },
[".png"] = new[] { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } },
[".gif"] = new[] { new byte[] { 0x47, 0x49, 0x46, 0x38 } },
[".bmp"] = new[] { new byte[] { 0x42, 0x4D } },
[".pdf"] = new[] { new byte[] { 0x25, 0x50, 0x44, 0x46 } },
[".zip"] = new[] { new byte[] { 0x50, 0x4B, 0x03, 0x04 } },
[".rar"] = new[] { new byte[] { 0x52, 0x61, 0x72, 0x21 } },
[".doc"] = new[] { new byte[] { 0xD0, 0xCF, 0x11, 0xE0 } },
[".docx"] = new[] { new byte[] { 0x50, 0x4B, 0x03, 0x04 } },
[".mp4"] = new[] { new byte[] { 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70 } }
};
public FileSecurityService(ILogger<FileSecurityService> logger)
{
_logger = logger;
}
/// <summary>
/// 综合安全校验
/// </summary>
public SecurityCheckResult PerformSecurityCheck(IFormFile file, SecurityOptions options = null)
{
options ??= new SecurityOptions();
var result = new SecurityCheckResult { IsSafe = true };
// 1. 检查文件名安全性
var nameCheck = CheckFileName(file.FileName);
if (!nameCheck.IsSafe)
{
result.IsSafe = false;
result.Issues.AddRange(nameCheck.Issues);
}
// 2. 检查扩展名
var extCheck = CheckExtension(file.FileName);
if (!extCheck.IsSafe)
{
result.IsSafe = false;
result.Issues.AddRange(extCheck.Issues);
}
// 3. 检查文件大小
if (file.Length > options.MaxFileSize)
{
result.IsSafe = false;
result.Issues.Add($"文件大小超过限制 ({options.MaxFileSize / 1024 / 1024}MB)");
}
// 4. 检查文件内容(需要读取文件)
if (options.CheckContent)
{
var contentCheck = CheckFileContent(file);
if (!contentCheck.IsSafe)
{
result.IsSafe = false;
result.Issues.AddRange(contentCheck.Issues);
}
}
return result;
}
/// <summary>
/// 检查文件名安全性
/// </summary>
private SecurityCheckResult CheckFileName(string fileName)
{
var result = new SecurityCheckResult { IsSafe = true };
// 检查路径遍历攻击
if (fileName.Contains("..") || fileName.Contains("/") || fileName.Contains("\\\\"))
{
result.IsSafe = false;
result.Issues.Add("文件名包含非法字符");
}
// 检查文件名长度
if (fileName.Length > 255)
{
result.IsSafe = false;
result.Issues.Add("文件名过长");
}
// 检查特殊字符
var invalidChars = Path.GetInvalidFileNameChars();
if (fileName.IndexOfAny(invalidChars) >= 0)
{
result.IsSafe = false;
result.Issues.Add("文件名包含非法字符");
}
return result;
}
/// <summary>
/// 检查扩展名安全性
/// </summary>
private SecurityCheckResult CheckExtension(string fileName)
{
var result = new SecurityCheckResult { IsSafe = true };
var extension = Path.GetExtension(fileName).ToLowerInvariant();
if (DangerousExtensions.Contains(extension))
{
result.IsSafe = false;
result.Issues.Add($"禁止上传 {extension} 类型的文件");
}
// 检查双扩展名攻击(如:file.jpg.exe)
var multiExt = fileName.Split('.');
if (multiExt.Length > 2)
{
foreach (var ext in multiExt.Skip(1).Select(e => $".{e.ToLower()}"))
{
if (DangerousExtensions.Contains(ext))
{
result.IsSafe = false;
result.Issues.Add("检测到可疑的双扩展名");
break;
}
}
}
return result;
}
/// <summary>
/// 检查文件内容(魔数校验)
/// </summary>
private SecurityCheckResult CheckFileContent(IFormFile file)
{
var result = new SecurityCheckResult { IsSafe = true };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!FileSignatures.ContainsKey(extension))
{
// 未配置签名的类型跳过检查
return result;
}
try
{
using var stream = file.OpenReadStream();
var header = new byte[16];
var bytesRead = stream.Read(header, 0, header.Length);
if (bytesRead == 0)
{
result.IsSafe = false;
result.Issues.Add("文件内容为空");
return result;
}
var signatures = FileSignatures[extension];
var matchesAnySignature = signatures.Any(sig =>
bytesRead >= sig.Length &&
header.Take(sig.Length).SequenceEqual(sig));
if (!matchesAnySignature)
{
result.IsSafe = false;
result.Issues.Add("文件内容与扩展名不匹配");
_logger.LogWarning($"文件内容校验失败: {file.FileName}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "文件内容检查异常");
result.IsSafe = false;
result.Issues.Add("文件内容检查失败");
}
return result;
}
/// <summary>
/// 计算文件哈希值
/// </summary>
public async Task<string> CalculateFileHashAsync(string filePath, string algorithm = "SHA256")
{
using var stream = File.OpenRead(filePath);
using var hashAlgorithm = algorithm.ToUpperInvariant() switch
{
"MD5" => System.Security.Cryptography.MD5.Create(),
"SHA1" => System.Security.Cryptography.SHA1.Create(),
"SHA256" => System.Security.Cryptography.SHA256.Create(),
"SHA512" => System.Security.Cryptography.SHA512.Create(),
_ => System.Security.Cryptography.SHA256.Create()
};
var hashBytes = await hashAlgorithm.ComputeHashAsync(stream);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
public class SecurityOptions
{
public long MaxFileSize { get; set; } = 100 * 1024 * 1024; // 100MB
public bool CheckContent { get; set; } = true;
public bool ScanVirus { get; set; } = false;
}
public class SecurityCheckResult
{
public bool IsSafe { get; set; }
public List<string> Issues { get; set; } = new();
}
5.2 病毒扫描集成(示例)
/// <summary>
/// 病毒扫描服务(ClamAV 示例)
/// </summary>
public class VirusScanService
{
private readonly ILogger<VirusScanService> _logger;
private readonly string _clamAvHost;
private readonly int _clamAvPort;
public VirusScanService(IConfiguration config, ILogger<VirusScanService> logger)
{
_clamAvHost = config["ClamAV:Host"] ?? "localhost";
_clamAvPort = config.GetValue("ClamAV:Port", 3310);
_logger = logger;
}
/// <summary>
/// 扫描文件
/// </summary>
public async Task<VirusScanResult> ScanFileAsync(string filePath)
{
try
{
using var client = new TcpClient(_clamAvHost, _clamAvPort);
await using var stream = client.GetStream();
// 发送扫描命令
var scanCommand = $"SCAN {filePath}\\n";
var commandBytes = Encoding.ASCII.GetBytes(scanCommand);
await stream.WriteAsync(commandBytes, 0, commandBytes.Length);
// 读取响应
var buffer = new byte[1024];
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
var response = Encoding.ASCII.GetString(buffer, 0, bytesRead).Trim();
// 解析结果
var isClean = response.Contains("OK") && !response.Contains("FOUND");
var virusName = isClean ? null : response.Split(':').Last().Trim();
_logger.LogInformation($"病毒扫描完成: {filePath} – {(isClean ? "安全" : $"发现病毒: {virusName}")}");
return new VirusScanResult
{
IsClean = isClean,
VirusName = virusName,
ScanTime = DateTime.Now
};
}
catch (Exception ex)
{
_logger.LogError(ex, "病毒扫描失败");
return new VirusScanResult
{
IsClean = false,
Error = "扫描服务不可用"
};
}
}
}
public class VirusScanResult
{
public bool IsClean { get; set; }
public string VirusName { get; set; }
public string Error { get; set; }
public DateTime ScanTime { get; set; }
}
第六部分:云存储集成
6.1 阿里云 OSS 集成
首先安装 SDK:
dotnet add package Aliyun.OSS.SDK.NetCore
创建 OSS 服务:
using Aliyun.OSS;
/// <summary>
/// 阿里云 OSS 文件存储服务
/// </summary>
public class AliyunOssService
{
private readonly OssClient _ossClient;
private readonly string _bucketName;
private readonly ILogger<AliyunOssService> _logger;
public AliyunOssService(IConfiguration config, ILogger<AliyunOssService> logger)
{
var endpoint = config["AliyunOSS:Endpoint"];
var accessKeyId = config["AliyunOSS:AccessKeyId"];
var accessKeySecret = config["AliyunOSS:AccessKeySecret"];
_bucketName = config["AliyunOSS:BucketName"];
_ossClient = new OssClient(endpoint, accessKeyId, accessKeySecret);
_logger = logger;
}
/// <summary>
/// 上传文件
/// </summary>
public async Task<OssUploadResult> UploadAsync(IFormFile file, string folder = "")
{
try
{
var extension = Path.GetExtension(file.FileName);
var objectKey = string.IsNullOrEmpty(folder)
? $"{Guid.NewGuid()}{extension}"
: $"{folder}/{Guid.NewGuid()}{extension}";
// 使用流式上传
await using var stream = file.OpenReadStream();
var request = new PutObjectRequest(_bucketName, objectKey, stream)
{
ContentType = file.ContentType
};
// 设置元数据
request.Metadata.Add("x-oss-meta-original-filename", file.FileName);
request.Metadata.Add("x-oss-meta-upload-time", DateTime.Now.ToString("o"));
var result = _ossClient.PutObject(request);
_logger.LogInformation($"OSS 上传成功: {objectKey}, ETag: {result.ETag}");
return new OssUploadResult
{
Success = true,
ObjectKey = objectKey,
Url = $"https://{_bucketName}.oss-cn-hangzhou.aliyuncs.com/{objectKey}",
ETag = result.ETag
};
}
catch (Exception ex)
{
_logger.LogError(ex, "OSS 上传失败");
return new OssUploadResult { Success = false, Error = ex.Message };
}
}
/// <summary>
/// 分片上传(大文件)
/// </summary>
public async Task<OssUploadResult> MultipartUploadAsync(
IFormFile file,
string folder = "",
IProgress<long> progress = null)
{
try
{
var extension = Path.GetExtension(file.FileName);
var objectKey = string.IsNullOrEmpty(folder)
? $"{Guid.NewGuid()}{extension}"
: $"{folder}/{Guid.NewGuid()}{extension}";
// 初始化分片上传
var initiateRequest = new InitiateMultipartUploadRequest(_bucketName, objectKey);
var initiateResult = _ossClient.InitiateMultipartUpload(initiateRequest);
var uploadId = initiateResult.UploadId;
// 分片大小:5MB
const long partSize = 5 * 1024 * 1024;
var partETags = new List<PartETag>();
var partNumber = 1;
await using var stream = file.OpenReadStream();
var buffer = new byte[partSize];
long totalRead = 0;
while (true)
{
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break;
using var partStream = new MemoryStream(buffer, 0, bytesRead);
var uploadPartRequest = new UploadPartRequest(
_bucketName,
objectKey,
uploadId,
partNumber,
partStream)
{
PartSize = bytesRead
};
var uploadPartResult = _ossClient.UploadPart(uploadPartRequest);
partETags.Add(uploadPartResult.PartETag);
totalRead += bytesRead;
progress?.Report(totalRead);
partNumber++;
}
// 完成分片上传
var completeRequest = new CompleteMultipartUploadRequest(
_bucketName,
objectKey,
uploadId,
partETags);
var completeResult = _ossClient.CompleteMultipartUpload(completeRequest);
return new OssUploadResult
{
Success = true,
ObjectKey = objectKey,
Url = $"https://{_bucketName}.oss-cn-hangzhou.aliyuncs.com/{objectKey}",
ETag = completeResult.ETag
};
}
catch (Exception ex)
{
_logger.LogError(ex, "OSS 分片上传失败");
return new OssUploadResult { Success = false, Error = ex.Message };
}
}
/// <summary>
/// 下载文件
/// </summary>
public async Task<Stream> DownloadAsync(string objectKey)
{
try
{
var request = new GetObjectRequest(_bucketName, objectKey);
var result = _ossClient.GetObject(request);
return result.Content;
}
catch (Exception ex)
{
_logger.LogError(ex, $"OSS 下载失败: {objectKey}");
throw;
}
}
/// <summary>
/// 生成临时访问 URL(签名 URL)
/// </summary>
public string GeneratePresignedUrl(string objectKey, int expirationMinutes = 60)
{
var request = new GeneratePresignedUriRequest(_bucketName, objectKey, SignHttpMethod.Get)
{
Expiration = DateTime.Now.AddMinutes(expirationMinutes)
};
return _ossClient.GeneratePresignedUri(request).AbsoluteUri;
}
/// <summary>
/// 删除文件
/// </summary>
public bool Delete(string objectKey)
{
try
{
_ossClient.DeleteObject(_bucketName, objectKey);
_logger.LogInformation($"OSS 删除成功: {objectKey}");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"OSS 删除失败: {objectKey}");
return false;
}
}
}
public class OssUploadResult
{
public bool Success { get; set; }
public string ObjectKey { get; set; }
public string Url { get; set; }
public string ETag { get; set; }
public string Error { get; set; }
}
6.2 Azure Blob Storage 集成
安装 SDK:
dotnet add package Azure.Storage.Blobs
创建 Azure Blob 服务:
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
/// <summary>
/// Azure Blob 存储服务
/// </summary>
public class AzureBlobService
{
private readonly BlobServiceClient _blobServiceClient;
private readonly BlobContainerClient _containerClient;
private readonly ILogger<AzureBlobService> _logger;
public AzureBlobService(IConfiguration config, ILogger<AzureBlobService> logger)
{
var connectionString = config["AzureBlob:ConnectionString"];
var containerName = config["AzureBlob:ContainerName"];
_blobServiceClient = new BlobServiceClient(connectionString);
_containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
// 确保容器存在
_containerClient.CreateIfNotExists();
_logger = logger;
}
/// <summary>
/// 上传文件
/// </summary>
public async Task<BlobUploadResult> UploadAsync(IFormFile file, string folder = "")
{
try
{
var extension = Path.GetExtension(file.FileName);
var blobName = string.IsNullOrEmpty(folder)
? $"{Guid.NewGuid()}{extension}"
: $"{folder}/{Guid.NewGuid()}{extension}";
var blobClient = _containerClient.GetBlobClient(blobName);
await using var stream = file.OpenReadStream();
var options = new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders
{
ContentType = file.ContentType
},
Metadata = new Dictionary<string, string>
{
["OriginalFileName"] = file.FileName,
["UploadTime"] = DateTime.Now.ToString("o")
}
};
var response = await blobClient.UploadAsync(stream, options);
_logger.LogInformation($"Azure Blob 上传成功: {blobName}");
return new BlobUploadResult
{
Success = true,
BlobName = blobName,
Url = blobClient.Uri.ToString(),
BlobType = "BlockBlob"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Azure Blob 上传失败");
return new BlobUploadResult { Success = false, Error = ex.Message };
}
}
/// <summary>
/// 大文件分块上传
/// </summary>
public async Task<BlobUploadResult> LargeFileUploadAsync(
IFormFile file,
string folder = "",
IProgress<long> progress = null)
{
try
{
var extension = Path.GetExtension(file.FileName);
var blobName = string.IsNullOrEmpty(folder)
? $"{Guid.NewGuid()}{extension}"
: $"{folder}/{Guid.NewGuid()}{extension}";
var blobClient = _containerClient.GetBlobClient(blobName);
await using var stream = file.OpenReadStream();
var options = new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders { ContentType = file.ContentType },
TransferOptions = new StorageTransferOptions
{
MaximumConcurrency = 4,
MaximumTransferSize = 4 * 1024 * 1024 // 4MB 块大小
},
ProgressHandler = progress
};
await blobClient.UploadAsync(stream, options);
return new BlobUploadResult
{
Success = true,
BlobName = blobName,
Url = blobClient.Uri.ToString()
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Azure Blob 大文件上传失败");
return new BlobUploadResult { Success = false, Error = ex.Message };
}
}
/// <summary>
/// 下载文件
/// </summary>
public async Task<Stream> DownloadAsync(string blobName)
{
var blobClient = _containerClient.GetBlobClient(blobName);
var response = await blobClient.DownloadAsync();
return response.Value.Content;
}
/// <summary>
/// 生成 SAS 令牌 URL
/// </summary>
public string GenerateSasUrl(string blobName, int expirationMinutes = 60)
{
var blobClient = _containerClient.GetBlobClient(blobName);
var sasBuilder = new Azure.Storage.Sas.BlobSasBuilder
{
BlobContainerName = _containerClient.Name,
BlobName = blobName,
Resource = Azure.Storage.Sas.BlobSasResourceType.Blob,
ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(expirationMinutes)
};
sasBuilder.SetPermissions(Azure.Storage.Sas.BlobSasPermissions.Read);
return blobClient.GenerateSasUri(sasBuilder).ToString();
}
/// <summary>
/// 删除文件
/// </summary>
public async Task<bool> DeleteAsync(string blobName)
{
try
{
var blobClient = _containerClient.GetBlobClient(blobName);
await blobClient.DeleteAsync();
_logger.LogInformation($"Azure Blob 删除成功: {blobName}");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Azure Blob 删除失败: {blobName}");
return false;
}
}
/// <summary>
/// 列出容器中的所有文件
/// </summary>
public async Task<List<BlobInfo>> ListBlobsAsync(string prefix = "")
{
var blobs = new List<BlobInfo>();
await foreach (var blob in _containerClient.GetBlobsAsync(prefix: prefix))
{
blobs.Add(new BlobInfo
{
Name = blob.Name,
Size = blob.Properties.ContentLength ?? 0,
ContentType = blob.Properties.ContentType,
LastModified = blob.Properties.LastModified ?? DateTimeOffset.MinValue
});
}
return blobs;
}
}
public class BlobUploadResult
{
public bool Success { get; set; }
public string BlobName { get; set; }
public string Url { get; set; }
public string BlobType { get; set; }
public string Error { get; set; }
}
public class BlobInfo
{
public string Name { get; set; }
public long Size { get; set; }
public string ContentType { get; set; }
public DateTimeOffset LastModified { get; set; }
}
6.3 统一存储接口
/// <summary>
/// 文件存储接口(支持多种存储后端)
/// </summary>
public interface IFileStorageService
{
Task<StorageResult> UploadAsync(IFormFile file, string path = "");
Task<Stream> DownloadAsync(string path);
Task<bool> DeleteAsync(string path);
string GetAccessUrl(string path, int expirationMinutes = 60);
}
/// <summary>
/// 存储服务工厂
/// </summary>
public class StorageServiceFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly IConfiguration _config;
public StorageServiceFactory(IServiceProvider serviceProvider, IConfiguration config)
{
_serviceProvider = serviceProvider;
_config = config;
}
public IFileStorageService Create(StorageType type)
{
return type switch
{
StorageType.Local => _serviceProvider.GetRequiredService<LocalStorageService>(),
StorageType.AliyunOss => _serviceProvider.GetRequiredService<AliyunOssStorageService>(),
StorageType.AzureBlob => _serviceProvider.GetRequiredService<AzureBlobStorageService>(),
_ => throw new ArgumentException($"不支持的存储类型: {type}")
};
}
public IFileStorageService CreateDefault()
{
var defaultType = _config.GetValue<StorageType>("Storage:Default");
return Create(defaultType);
}
}
public enum StorageType
{
Local,
AliyunOss,
AzureBlob
}
public class StorageResult
{
public bool Success { get; set; }
public string Path { get; set; }
public string Url { get; set; }
public string Error { get; set; }
}
第七部分:实战案例 – 用户头像上传系统
7.1 头像上传服务
/// <summary>
/// 用户头像上传服务
/// </summary>
public class AvatarService
{
private readonly IWebHostEnvironment _environment;
private readonly ILogger<AvatarService> _logger;
private readonly FileSecurityService _securityService;
private readonly string _avatarFolder;
// 头像尺寸配置
private static readonly int[] AvatarSizes = { 32, 64, 128, 256, 512 };
public AvatarService(
IWebHostEnvironment environment,
ILogger<AvatarService> logger,
FileSecurityService securityService)
{
_environment = environment;
_logger = logger;
_securityService = securityService;
_avatarFolder = Path.Combine(environment.WebRootPath, "uploads", "avatars");
Directory.CreateDirectory(_avatarFolder);
}
/// <summary>
/// 上传并处理头像
/// </summary>
public async Task<AvatarUploadResult> UploadAvatarAsync(IFormFile file, int userId)
{
// 1. 安全校验
var securityResult = _securityService.PerformSecurityCheck(file, new SecurityOptions
{
MaxFileSize = 5 * 1024 * 1024, // 5MB
CheckContent = true
});
if (!securityResult.IsSafe)
{
return new AvatarUploadResult
{
Success = false,
Errors = securityResult.Issues
};
}
// 2. 验证图片类型
var allowedTypes = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedTypes.Contains(extension))
{
return new AvatarUploadResult
{
Success = false,
Errors = new List<string> { "只支持 JPG、PNG、GIF、WebP 格式的图片" }
};
}
try
{
// 3. 生成文件名
var fileName = $"{userId}_{Guid.NewGuid():N}{extension}";
var originalPath = Path.Combine(_avatarFolder, "original", fileName);
Directory.CreateDirectory(Path.GetDirectoryName(originalPath)!);
// 4. 保存原图
await using (var stream = new FileStream(originalPath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
// 5. 生成多尺寸缩略图
var thumbnails = new Dictionary<int, string>();
foreach (var size in AvatarSizes)
{
var thumbnailPath = Path.Combine(_avatarFolder, $"{size}x{size}", fileName);
Directory.CreateDirectory(Path.GetDirectoryName(thumbnailPath)!);
await GenerateThumbnailAsync(originalPath, thumbnailPath, size, size);
thumbnails[size] = $"/uploads/avatars/{size}x{size}/{fileName}";
}
_logger.LogInformation($"用户头像上传成功: UserId={userId}, File={fileName}");
return new AvatarUploadResult
{
Success = true,
OriginalUrl = $"/uploads/avatars/original/{fileName}",
Thumbnails = thumbnails,
FileName = fileName
};
}
catch (Exception ex)
{
_logger.LogError(ex, $"头像上传失败: UserId={userId}");
return new AvatarUploadResult
{
Success = false,
Errors = new List<string> { "头像处理失败,请重试" }
};
}
}
/// <summary>
/// 生成缩略图(使用 System.Drawing 或 SkiaSharp)
/// </summary>
private async Task GenerateThumbnailAsync(string sourcePath, string targetPath, int width, int height)
{
// 这里使用简单的示例,生产环境建议使用 SkiaSharp 或 ImageSharp
// 以下是使用 SkiaSharp 的示例代码:
/*
using var original = SKBitmap.Decode(sourcePath);
using var resized = original.Resize(new SKSizeI(width, height), SKFilterQuality.High);
using var image = SKImage.FromBitmap(resized);
using var data = image.Encode(SKEncodedImageFormat.Png, 90);
await File.WriteAllBytesAsync(targetPath, data.ToArray());
*/
// 简化版:直接复制(实际项目中应使用图像处理库)
await using var sourceStream = File.OpenRead(sourcePath);
await using var targetStream = File.Create(targetPath);
await sourceStream.CopyToAsync(targetStream);
}
/// <summary>
/// 删除用户头像
/// </summary>
public bool DeleteAvatar(int userId)
{
try
{
var pattern = $"{userId}_*";
var files = Directory.GetFiles(_avatarFolder, pattern, SearchOption.AllDirectories);
foreach (var file in files)
{
File.Delete(file);
}
_logger.LogInformation($"用户头像删除成功: UserId={userId}");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"头像删除失败: UserId={userId}");
return false;
}
}
}
public class AvatarUploadResult
{
public bool Success { get; set; }
public string OriginalUrl { get; set; }
public Dictionary<int, string> Thumbnails { get; set; }
public string FileName { get; set; }
public List<string> Errors { get; set; }
}
7.2 头像上传控制器
[ApiController]
[Route("api/avatar")]
public class AvatarController : ControllerBase
{
private readonly AvatarService _avatarService;
private readonly ILogger<AvatarController> _logger;
public AvatarController(AvatarService avatarService, ILogger<AvatarController> logger)
{
_avatarService = avatarService;
_logger = logger;
}
/// <summary>
/// 上传头像
/// </summary>
[HttpPost("upload")]
[RequestSizeLimit(10 * 1024 * 1024)] // 10MB
public async Task<IActionResult> Upload([FromQuery] int userId, IFormFile file)
{
if (userId <= 0)
{
return BadRequest(new { code = 400, message = "用户ID无效" });
}
var result = await _avatarService.UploadAvatarAsync(file, userId);
if (!result.Success)
{
return BadRequest(new
{
code = 400,
message = "上传失败",
errors = result.Errors
});
}
return Ok(new
{
code = 200,
message = "头像上传成功",
data = new
{
original = result.OriginalUrl,
small = result.Thumbnails.GetValueOrDefault(64),
medium = result.Thumbnails.GetValueOrDefault(128),
large = result.Thumbnails.GetValueOrDefault(256)
}
});
}
/// <summary>
/// 删除头像
/// </summary>
[HttpDelete("{userId}")]
public IActionResult Delete(int userId)
{
var success = _avatarService.DeleteAvatar(userId);
if (!success)
{
return StatusCode(500, new { code = 500, message = "删除失败" });
}
return Ok(new { code = 200, message = "头像删除成功" });
}
}
7.3 配置与注册
// Program.cs 或 Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// 注册文件服务
services.AddSingleton<FileValidationService>();
services.AddSingleton<FileSecurityService>();
services.AddSingleton<ChunkUploadService>();
services.AddSingleton<AvatarService>();
// 注册云存储服务
services.AddSingleton<AliyunOssService>();
services.AddSingleton<AzureBlobService>();
// 配置文件上传限制
services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100MB
options.ValueLengthLimit = int.MaxValue;
options.MultipartHeadersLengthLimit = int.MaxValue;
});
// Kestrel 配置
services.Configure<KestrelServerOptions>(options =>
{
options.Limits.MaxRequestBodySize = 100 * 1024 * 1024; // 100MB
});
}
总结
本文详细介绍了 C# 中文件上传下载的完整解决方案,涵盖以下核心内容:
核心要点回顾
| 单文件上传 | IFormFile、流式保存 | 文件类型、大小校验 |
| 多文件上传 | 批量处理、进度追踪 | 数量限制、异常处理 |
| 大文件分片 | 分片上传、断点续传 | 临时文件清理、合并策略 |
| 断点续传 | Range 请求、部分流 | HTTP 206 状态码 |
| 安全校验 | 魔数校验、路径过滤 | 防止上传恶意文件 |
| 云存储 | OSS SDK、Blob SDK | 分片上传、签名 URL |
最佳实践建议
参考资源
- ASP.NET Core 文件上传官方文档
- 阿里云 OSS .NET SDK
- Azure Blob Storage .NET SDK
- SkiaSharp 图像处理库
- ImageSharp 图像处理库
关注引导
如果本文对你有帮助,欢迎:
- 点赞 👍:让更多人看到这篇教程
- 收藏 ⭐:方便日后查阅复习
- 评论 💬:分享你的见解或问题
- 关注 🔔:获取更多 C# 实战教程
作者简介:专注 .NET 技术栈 10+ 年,分享最实用的开发技巧和最佳实践。
下一篇预告
下一篇:C# Excel 导入导出实战
预告内容:
- EPPlus 与 NPOI 对比选型
- 大数据量 Excel 导入优化
- Excel 模板导出与动态生成
- 复杂报表设计与样式定制
- 性能优化:流式读写与内存管理
敬请期待!
标签:C# 文件上传 文件下载 分片上传 断点续传 阿里云OSS Azure Blob ASP.NET Core



