第22章:文件服务与附件处理
本章目标
学完本章后,你将能够:
- 实现文件下载(带文件名)
- 理解断点续传的原理
- 了解大文件分片上传的思路
- 区分文件预览和下载的场景
前置知识
- 需要先学习:第20章(文件上传)
22.1 文件下载
go
// 直接下载
r.GET("/download/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := "./uploads/" + filename
// 检查文件是否存在
if _, err := os.Stat(filepath); os.IsNotExist(err) {
c.JSON(404, gin.H{"error": "文件不存在"})
return
}
// 方式一:自动识别 Content-Type
c.File(filepath)
// 方式二:指定下载文件名
// c.FileAttachment(filepath, filename)
})22.2 区分预览和下载
go
r.GET("/file/:filename", func(c *gin.Context) {
filename := c.Param("filename")
action := c.DefaultQuery("action", "inline") // inline=预览, download=下载
if action == "download" {
c.Header("Content-Disposition", "attachment; filename="+filename)
} else {
c.Header("Content-Disposition", "inline")
}
c.File("./uploads/" + filename)
})22.3 断点续传思路
go
r.GET("/video/:filename", func(c *gin.Context) {
filepath := "./uploads/" + c.Param("filename")
// 获取 Range 请求头
rangeHeader := c.GetHeader("Range")
// 解析 Range,只返回请求的字节范围
// 实际项目中用 http.ServeFile 或专门的流媒体服务器
http.ServeFile(c.Writer, c.Request, filepath)
})22.4 大文件分片上传思路
分片上传的核心思路:前端把大文件切成小块,逐块上传,服务端记录每块的状态,最后合并:
go
// 1. 初始化分片上传
r.POST("/upload/init", func(c *gin.Context) {
// 生成 uploadID,返回给客户端
})
// 2. 上传每个分片
r.POST("/upload/chunk", func(c *gin.Context) {
// uploadID + chunkIndex + chunkData
// 保存为:uploads/{uploadID}/chunk_{index}
})
// 3. 合并分片
r.POST("/upload/merge", func(c *gin.Context) {
// 将所有分片合并为完整文件
// 校验 MD5,返回文件 URL
})22.5 文件服务最佳实践
| 场景 | 方案 |
|---|---|
| 小文件(<10MB) | 本地磁盘 + Gin 静态服务 |
| 中等文件(<100MB) | 对象存储(OSS/S3)+ 签名 URL |
| 大文件(>100MB) | 对象存储 + 分片上传 |
| 视频/音频 | CDN + 流媒体服务器 |
本章小结
c.File()文件下载/预览Content-Disposition: attachment强制下载- 断点续传需要解析
Range请求头 - 大文件用分片上传 + 合并策略
- 生产环境文件推荐存对象存储(OSS/S3)
练习题
- 实现文件下载接口,支持指定文件名。
- 实现一个根据
action=download参数控制预览/下载的接口。 - 描述大文件分片上传的流程(不需要写完整代码)。
- (思考题)为什么大多数 Web 应用不把用户上传的文件直接存在 Web 服务器的磁盘上?