5.9 KiB
5.9 KiB
服务器文件系统存储实现
✅ 已完成
成功实现了将图片存储在服务器文件系统的功能。
📁 文件结构
chinese-family-tree/
├── app/
│ └── api/
│ └── upload/
│ └── route.ts # 上传 API
├── public/
│ └── uploads/ # 上传文件存储目录
│ └── .gitkeep # 保留目录结构
└── .gitignore # 忽略上传的文件
🔧 实现细节
1. 上传 API (app/api/upload/route.ts)
功能:
- 接收文件上传请求
- 验证用户登录状态
- 验证文件类型(只允许图片)
- 验证文件大小(限制 5MB)
- 生成唯一文件名
- 保存到
public/uploads目录 - 返回可访问的 URL
安全措施:
- ✅ 用户身份验证
- ✅ 文件类型验证
- ✅ 文件大小限制
- ✅ 唯一文件名(防止覆盖)
文件命名规则:
{timestamp}-{random}.{ext}
例如:1732435200000-a3b5c7.jpg
2. 前端上传逻辑
修改文件: app/members/[id]/page.tsx
流程:
- 用户选择图片
- 创建 FormData
- 调用
/api/uploadAPI - 获取返回的 URL
- 更新成员的
avatarUrl字段 - 显示新头像
兼容性:
- 优先使用
avatarUrl(新方案) - 兼容
avatarImageId(旧的 IndexedDB 方案)
3. Git 配置
.gitignore 规则:
# Uploads
public/uploads/*
!public/uploads/.gitkeep
说明:
- 忽略所有上传的文件
- 保留
.gitkeep文件以维持目录结构 - 避免将用户上传的图片提交到 Git
📊 数据库字段
FamilyMember 表
| 字段 | 类型 | 说明 |
|---|---|---|
avatarUrl |
String? | 头像 URL(新方案) |
avatarImageId |
String? | IndexedDB ID(旧方案,兼容) |
推荐: 使用 avatarUrl 字段
🎯 使用方式
上传头像
- 访问成员详情页
- 将鼠标悬停在头像上
- 点击出现的相机图标
- 选择图片文件
- 自动上传并更新
API 调用示例
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const { url } = await response.json()
// url: "/uploads/1732435200000-a3b5c7.jpg"
🔒 安全性
已实现
- 身份验证:需要登录才能上传
- 文件类型验证:只允许图片格式
- 文件大小限制:最大 5MB
- 唯一文件名:防止文件覆盖
建议增强
- 图片压缩:上传前压缩以节省空间
- 病毒扫描:扫描上传的文件
- 访问控制:限制只有家族成员可访问
- 定期清理:删除未使用的图片
📈 性能优化
当前实现
- ✅ 直接存储在
public目录 - ✅ Next.js 自动处理静态文件服务
- ✅ 支持浏览器缓存
可选优化
- 图片压缩
import imageCompression from 'browser-image-compression'
const compressed = await imageCompression(file, {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
})
- CDN 加速
- 使用 Nginx 反向代理
- 配置 CDN 服务
- 启用 Gzip 压缩
- 懒加载
<img
src={avatarUrl}
loading="lazy"
alt="头像"
/>
🚀 部署注意事项
1. 文件权限
确保 public/uploads 目录有写入权限:
chmod 755 public/uploads
2. 磁盘空间
监控磁盘使用情况:
du -sh public/uploads
3. 备份策略
定期备份上传的文件:
# 备份到其他位置
rsync -av public/uploads/ /backup/uploads/
# 或使用 tar 打包
tar -czf uploads-backup-$(date +%Y%m%d).tar.gz public/uploads/
4. Nginx 配置(可选)
如果使用 Nginx 作为反向代理:
location /uploads/ {
alias /path/to/app/public/uploads/;
expires 30d;
add_header Cache-Control "public, immutable";
}
🔄 迁移指南
从 IndexedDB 迁移
如果有旧数据使用 IndexedDB 存储:
- 导出旧数据
const images = await db.images.toArray()
for (const image of images) {
const formData = new FormData()
formData.append('file', new File([image.blob], 'image.jpg'))
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const { url } = await response.json()
// 更新成员记录
await updateMember(memberId, { avatarUrl: url })
}
- 清理旧数据
await db.images.clear()
💡 最佳实践
1. 文件命名
- ✅ 使用时间戳 + 随机字符串
- ✅ 保留原始文件扩展名
- ❌ 不要使用用户提供的文件名
2. 存储位置
- ✅
public/uploads/- 可直接访问 - ❌
private/uploads/- 需要额外处理
3. 错误处理
- ✅ 提供友好的错误提示
- ✅ 记录错误日志
- ✅ 失败时清理临时文件
4. 用户体验
- ✅ 显示上传进度
- ✅ 支持拖拽上传
- ✅ 预览上传的图片
📝 后续改进
短期
- 添加图片压缩
- 支持拖拽上传
- 显示上传进度
中期
- 图片裁剪功能
- 支持多图上传
- 图片管理界面
长期
- 迁移到云存储
- CDN 加速
- 图片处理服务
✨ 优势
- 简单易用:不需要额外的云服务
- 成本低:只需服务器存储空间
- 完全控制:数据完全在自己手中
- 易于备份:直接备份文件夹即可
- 快速部署:无需配置云服务
⚠️ 限制
- 扩展性:单服务器存储容量有限
- 性能:大量访问时可能需要 CDN
- 备份:需要自己管理备份策略
- 多服务器:需要共享存储或同步
🎉 总结
成功实现了服务器文件系统存储方案,适合:
- 自托管部署
- 中小型应用
- 对数据隐私有要求的场景
- 不想依赖第三方云服务
如果未来需要更好的扩展性,可以轻松迁移到云存储服务!