Skip to main content

文件上传与下载

文件上传

1. 单文件上传

[ApiController]
[Route("api/[controller]")]
public class FileUploadController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
private readonly ILogger<FileUploadController> _logger;

public FileUploadController(
IWebHostEnvironment environment,
ILogger<FileUploadController> logger)
{
_environment = environment;
_logger = logger;
}

[HttpPost("upload")]
[RequestSizeLimit(10 * 1024 * 1024)] // 限制 10MB
public async Task<IActionResult> Upload(IFormFile file)
{
if (file == null || file.Length == 0)
{
return BadRequest("请选择文件");
}

// 验证文件类型
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".pdf" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();

if (!allowedExtensions.Contains(extension))
{
return BadRequest("不支持的文件类型");
}

// 生成唯一文件名
var fileName = $"{Guid.NewGuid()}{extension}";
var uploadPath = Path.Combine(_environment.WebRootPath, "uploads");

// 确保目录存在
if (!Directory.Exists(uploadPath))
{
Directory.CreateDirectory(uploadPath);
}

var filePath = Path.Combine(uploadPath, fileName);

// 保存文件
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}

_logger.LogInformation("文件上传成功: {FileName}", fileName);

return Ok(new
{
FileName = fileName,
OriginalName = file.FileName,
Size = file.Length,
ContentType = file.ContentType,
Url = $"/uploads/{fileName}"
});
}
}

2. 多文件上传

[HttpPost("upload-multiple")]
[RequestSizeLimit(50 * 1024 * 1024)] // 限制总大小 50MB
public async Task<IActionResult> UploadMultiple(List<IFormFile> files)
{
if (files == null || files.Count == 0)
{
return BadRequest("请选择至少一个文件");
}

if (files.Count > 10)
{
return BadRequest("最多只能上传 10 个文件");
}

var uploadedFiles = new List<FileUploadResult>();

foreach (var file in files)
{
if (file.Length > 0)
{
try
{
var fileName = await SaveFileAsync(file);

uploadedFiles.Add(new FileUploadResult
{
FileName = fileName,
OriginalName = file.FileName,
Size = file.Length,
Success = true
});
}
catch (Exception ex)
{
_logger.LogError(ex, "文件上传失败: {FileName}", file.FileName);

uploadedFiles.Add(new FileUploadResult
{
OriginalName = file.FileName,
Success = false,
Error = "上传失败"
});
}
}
}

return Ok(new
{
TotalFiles = files.Count,
SuccessCount = uploadedFiles.Count(f => f.Success),
FailedCount = uploadedFiles.Count(f => !f.Success),
Files = uploadedFiles
});
}

public class FileUploadResult
{
public string? FileName { get; set; }
public string OriginalName { get; set; } = string.Empty;
public long Size { get; set; }
public bool Success { get; set; }
public string? Error { get; set; }
}

3. 大文件分片上传

public class ChunkUploadDto
{
public IFormFile File { get; set; } = null!;
public int ChunkIndex { get; set; }
public int TotalChunks { get; set; }
public string FileIdentifier { get; set; } = string.Empty;
}

[HttpPost("upload-chunk")]
public async Task<IActionResult> UploadChunk([FromForm] ChunkUploadDto dto)
{
var tempPath = Path.Combine(_environment.WebRootPath, "temp", dto.FileIdentifier);
Directory.CreateDirectory(tempPath);

// 保存分片
var chunkPath = Path.Combine(tempPath, $"chunk_{dto.ChunkIndex}");
using (var stream = new FileStream(chunkPath, FileMode.Create))
{
await dto.File.CopyToAsync(stream);
}

_logger.LogInformation(
"分片上传: {ChunkIndex}/{TotalChunks}",
dto.ChunkIndex + 1,
dto.TotalChunks);

// 检查是否所有分片都已上传
var uploadedChunks = Directory.GetFiles(tempPath, "chunk_*").Length;

if (uploadedChunks == dto.TotalChunks)
{
// 合并分片
var fileName = await MergeChunksAsync(dto.FileIdentifier, dto.TotalChunks);

// 清理临时文件
Directory.Delete(tempPath, true);

return Ok(new
{
IsComplete = true,
FileName = fileName,
Message = "文件上传完成"
});
}

return Ok(new
{
IsComplete = false,
UploadedChunks = uploadedChunks,
TotalChunks = dto.TotalChunks
});
}

private async Task<string> MergeChunksAsync(string fileIdentifier, int totalChunks)
{
var tempPath = Path.Combine(_environment.WebRootPath, "temp", fileIdentifier);
var fileName = $"{Guid.NewGuid()}.bin";
var uploadPath = Path.Combine(_environment.WebRootPath, "uploads");
Directory.CreateDirectory(uploadPath);

var finalPath = Path.Combine(uploadPath, fileName);

using (var finalStream = new FileStream(finalPath, FileMode.Create))
{
for (int i = 0; i < totalChunks; i++)
{
var chunkPath = Path.Combine(tempPath, $"chunk_{i}");
using var chunkStream = new FileStream(chunkPath, FileMode.Open);
await chunkStream.CopyToAsync(finalStream);
}
}

_logger.LogInformation("分片合并完成: {FileName}", fileName);
return fileName;
}

4. 文件验证

public class FileValidator
{
private static readonly Dictionary<string, List<byte[]>> _fileSignatures = new()
{
{ ".jpg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 }
}
},
{ ".png", new List<byte[]>
{
new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }
}
},
{ ".pdf", new List<byte[]>
{
new byte[] { 0x25, 0x50, 0x44, 0x46 }
}
}
};

public static bool IsValidFile(IFormFile file)
{
if (file == null || file.Length == 0)
return false;

var extension = Path.GetExtension(file.FileName).ToLowerInvariant();

if (!_fileSignatures.ContainsKey(extension))
return false;

using var reader = new BinaryReader(file.OpenReadStream());
var signatures = _fileSignatures[extension];
var headerBytes = reader.ReadBytes(signatures.Max(s => s.Length));

return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}

public static bool IsValidSize(IFormFile file, long maxSizeInBytes)
{
return file != null && file.Length > 0 && file.Length <= maxSizeInBytes;
}

public static bool HasValidExtension(IFormFile file, string[] allowedExtensions)
{
if (file == null)
return false;

var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
return allowedExtensions.Contains(extension);
}
}

// 使用验证器
[HttpPost("upload-validated")]
public async Task<IActionResult> UploadValidated(IFormFile file)
{
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".pdf" };
var maxSize = 5 * 1024 * 1024; // 5MB

if (!FileValidator.HasValidExtension(file, allowedExtensions))
{
return BadRequest("不支持的文件类型");
}

if (!FileValidator.IsValidSize(file, maxSize))
{
return BadRequest("文件大小超出限制(最大 5MB)");
}

if (!FileValidator.IsValidFile(file))
{
return BadRequest("文件内容验证失败");
}

var fileName = await SaveFileAsync(file);

return Ok(new { FileName = fileName });
}

文件下载

1. 基本下载

[HttpGet("download/{fileName}")]
public IActionResult Download(string fileName)
{
// 验证文件名,防止路径遍历攻击
if (fileName.Contains("..") || fileName.Contains("/") || fileName.Contains("\\"))
{
return BadRequest("无效的文件名");
}

var filePath = Path.Combine(_environment.WebRootPath, "uploads", fileName);

if (!System.IO.File.Exists(filePath))
{
return NotFound("文件不存在");
}

var memory = new MemoryStream();
using (var stream = new FileStream(filePath, FileMode.Open))
{
stream.CopyTo(memory);
}
memory.Position = 0;

var contentType = GetContentType(fileName);

return File(memory, contentType, fileName);
}

private string GetContentType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".pdf" => "application/pdf",
".jpg" => "image/jpeg",
".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".txt" => "text/plain",
".json" => "application/json",
".xml" => "application/xml",
".zip" => "application/zip",
_ => "application/octet-stream"
};
}

2. 从数据库下载

public class FileEntity
{
public int Id { get; set; }
public string FileName { get; set; } = string.Empty;
public string OriginalName { get; set; } = string.Empty;
public byte[] Content { get; set; } = Array.Empty<byte>();
public string ContentType { get; set; } = string.Empty;
public DateTime UploadedAt { get; set; }
}

[HttpGet("download-from-db/{id}")]
public async Task<IActionResult> DownloadFromDatabase(int id)
{
var file = await _dbContext.Files.FindAsync(id);

if (file == null)
{
return NotFound("文件不存在");
}

return File(file.Content, file.ContentType, file.OriginalName);
}

// 上传到数据库
[HttpPost("upload-to-db")]
public async Task<IActionResult> UploadToDatabase(IFormFile file)
{
if (file == null || file.Length == 0)
{
return BadRequest("请选择文件");
}

using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);

var fileEntity = new FileEntity
{
FileName = file.FileName,
OriginalName = file.FileName,
Content = memoryStream.ToArray(),
ContentType = file.ContentType,
UploadedAt = DateTime.UtcNow
};

_dbContext.Files.Add(fileEntity);
await _dbContext.SaveChangesAsync();

return Ok(new { Id = fileEntity.Id, Message = "文件上传成功" });
}

3. 生成文件并下载

[HttpGet("export/users")]
public IActionResult ExportUsers()
{
// 获取用户数据
var users = GetUsers();

// 生成 CSV
var csv = GenerateCsv(users);

var bytes = Encoding.UTF8.GetBytes(csv);
var fileName = $"users_{DateTime.Now:yyyyMMddHHmmss}.csv";

return File(bytes, "text/csv", fileName);
}

private string GenerateCsv(List<User> users)
{
var sb = new StringBuilder();
sb.AppendLine("ID,Name,Email,CreatedAt");

foreach (var user in users)
{
sb.AppendLine($"{user.Id},{user.Name},{user.Email},{user.CreatedAt:yyyy-MM-dd}");
}

return sb.ToString();
}

// Excel 导出(使用 EPPlus)
[HttpGet("export/users/excel")]
public IActionResult ExportUsersToExcel()
{
var users = GetUsers();

using var package = new ExcelPackage();
var worksheet = package.Workbook.Worksheets.Add("Users");

// 添加表头
worksheet.Cells[1, 1].Value = "ID";
worksheet.Cells[1, 2].Value = "Name";
worksheet.Cells[1, 3].Value = "Email";
worksheet.Cells[1, 4].Value = "Created At";

// 添加数据
for (int i = 0; i < users.Count; i++)
{
var user = users[i];
worksheet.Cells[i + 2, 1].Value = user.Id;
worksheet.Cells[i + 2, 2].Value = user.Name;
worksheet.Cells[i + 2, 3].Value = user.Email;
worksheet.Cells[i + 2, 4].Value = user.CreatedAt.ToString("yyyy-MM-dd");
}

var stream = new MemoryStream();
package.SaveAs(stream);
stream.Position = 0;

var fileName = $"users_{DateTime.Now:yyyyMMddHHmmss}.xlsx";

return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
}

云存储集成

Azure Blob Storage

dotnet add package Azure.Storage.Blobs
public class AzureBlobService
{
private readonly BlobServiceClient _blobServiceClient;
private readonly string _containerName;

public AzureBlobService(IConfiguration configuration)
{
var connectionString = configuration["AzureStorage:ConnectionString"];
_blobServiceClient = new BlobServiceClient(connectionString);
_containerName = configuration["AzureStorage:ContainerName"] ?? "uploads";
}

public async Task<string> UploadAsync(IFormFile file)
{
var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
await containerClient.CreateIfNotExistsAsync();

var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var blobClient = containerClient.GetBlobClient(fileName);

await blobClient.UploadAsync(file.OpenReadStream(), new BlobHttpHeaders
{
ContentType = file.ContentType
});

return blobClient.Uri.ToString();
}

public async Task<Stream> DownloadAsync(string fileName)
{
var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
var blobClient = containerClient.GetBlobClient(fileName);

var download = await blobClient.DownloadAsync();
return download.Value.Content;
}

public async Task DeleteAsync(string fileName)
{
var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
var blobClient = containerClient.GetBlobClient(fileName);

await blobClient.DeleteIfExistsAsync();
}
}

// 使用
[HttpPost("upload-to-azure")]
public async Task<IActionResult> UploadToAzure(IFormFile file, [FromServices] AzureBlobService blobService)
{
if (file == null || file.Length == 0)
{
return BadRequest("请选择文件");
}

var url = await blobService.UploadAsync(file);

return Ok(new { Url = url });
}

配置限制

Program.cs

var builder = WebApplication.CreateBuilder(args);

// 配置 Kestrel 限制
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 100 * 1024 * 1024; // 100MB
});

// 配置 FormOptions
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100MB
options.ValueLengthLimit = int.MaxValue;
options.MultipartHeadersLengthLimit = int.MaxValue;
});

appsettings.json

{
"FileUpload": {
"MaxFileSize": 10485760,
"AllowedExtensions": [".jpg", ".jpeg", ".png", ".pdf"],
"UploadPath": "uploads"
}
}

最佳实践

最佳实践清单

1. 安全性

// ✅ 好:验证文件类型和内容
public async Task<IActionResult> Upload(IFormFile file)
{
// 验证扩展名
var allowedExtensions = new[] { ".jpg", ".png" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
return BadRequest("不支持的文件类型");

// 验证文件签名
if (!FileValidator.IsValidFile(file))
return BadRequest("文件内容验证失败");

// 生成新文件名,不使用原始文件名
var fileName = $"{Guid.NewGuid()}{extension}";

// 保存文件
}

// ❌ 差:直接使用原始文件名
var filePath = Path.Combine(uploadPath, file.FileName); // 危险!

2. 大文件处理

// ✅ 好:流式处理
[HttpPost("upload-large")]
[DisableRequestSizeLimit]
public async Task<IActionResult> UploadLarge()
{
var file = Request.Form.Files[0];

using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream); // 流式复制,不占用大量内存

return Ok();
}

// ❌ 差:一次性加载到内存
var bytes = new byte[file.Length];
await file.OpenReadStream().ReadAsync(bytes); // 大文件会导致内存溢出

3. 错误处理

// ✅ 好:完善的错误处理
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
try
{
// 验证
if (file == null || file.Length == 0)
return BadRequest("请选择文件");

// 保存
var fileName = await SaveFileAsync(file);

return Ok(new { FileName = fileName });
}
catch (IOException ex)
{
_logger.LogError(ex, "文件保存失败");
return StatusCode(500, "文件保存失败");
}
catch (Exception ex)
{
_logger.LogError(ex, "上传失败");
return StatusCode(500, "上传失败");
}
}

4. 性能优化

// ✅ 好:使用缓冲
[HttpGet("download/{fileName}")]
public async Task<IActionResult> Download(string fileName)
{
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
return File(stream, contentType, fileName, enableRangeProcessing: true);
}

// ✅ 好:启用范围请求支持(断点续传)
return File(stream, contentType, fileName, enableRangeProcessing: true);

总结

关键要点
  • 始终验证文件类型和大小
  • 生成唯一文件名,不使用原始文件名
  • 大文件使用流式处理或分片上传
  • 配置合理的文件大小限制
  • 使用云存储服务存储大量文件
  • 启用范围请求支持断点续传
  • 记录日志便于问题排查

相关资源