Files
chinese-family-tree-2/SERVER_FILE_STORAGE_IMPLEMENTATION.md
T
freedakgmail 3d075c6076 0.0.8.5
2025-11-24 14:02:34 +08:00

5.9 KiB
Raw Blame History

服务器文件系统存储实现

已完成

成功实现了将图片存储在服务器文件系统的功能。

📁 文件结构

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

流程:

  1. 用户选择图片
  2. 创建 FormData
  3. 调用 /api/upload API
  4. 获取返回的 URL
  5. 更新成员的 avatarUrl 字段
  6. 显示新头像

兼容性:

  • 优先使用 avatarUrl(新方案)
  • 兼容 avatarImageId(旧的 IndexedDB 方案)

3. Git 配置

.gitignore 规则:

# Uploads
public/uploads/*
!public/uploads/.gitkeep

说明:

  • 忽略所有上传的文件
  • 保留 .gitkeep 文件以维持目录结构
  • 避免将用户上传的图片提交到 Git

📊 数据库字段

FamilyMember 表

字段 类型 说明
avatarUrl String? 头像 URL(新方案)
avatarImageId String? IndexedDB ID(旧方案,兼容)

推荐: 使用 avatarUrl 字段

🎯 使用方式

上传头像

  1. 访问成员详情页
  2. 将鼠标悬停在头像上
  3. 点击出现的相机图标
  4. 选择图片文件
  5. 自动上传并更新

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"

🔒 安全性

已实现

  1. 身份验证:需要登录才能上传
  2. 文件类型验证:只允许图片格式
  3. 文件大小限制:最大 5MB
  4. 唯一文件名:防止文件覆盖

建议增强

  1. 图片压缩:上传前压缩以节省空间
  2. 病毒扫描:扫描上传的文件
  3. 访问控制:限制只有家族成员可访问
  4. 定期清理:删除未使用的图片

📈 性能优化

当前实现

  • 直接存储在 public 目录
  • Next.js 自动处理静态文件服务
  • 支持浏览器缓存

可选优化

  1. 图片压缩
import imageCompression from 'browser-image-compression'

const compressed = await imageCompression(file, {
  maxSizeMB: 1,
  maxWidthOrHeight: 1920,
})
  1. CDN 加速
  • 使用 Nginx 反向代理
  • 配置 CDN 服务
  • 启用 Gzip 压缩
  1. 懒加载
<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 存储:

  1. 导出旧数据
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 })
}
  1. 清理旧数据
await db.images.clear()

💡 最佳实践

1. 文件命名

  • 使用时间戳 + 随机字符串
  • 保留原始文件扩展名
  • 不要使用用户提供的文件名

2. 存储位置

  • public/uploads/ - 可直接访问
  • private/uploads/ - 需要额外处理

3. 错误处理

  • 提供友好的错误提示
  • 记录错误日志
  • 失败时清理临时文件

4. 用户体验

  • 显示上传进度
  • 支持拖拽上传
  • 预览上传的图片

📝 后续改进

短期

  • 添加图片压缩
  • 支持拖拽上传
  • 显示上传进度

中期

  • 图片裁剪功能
  • 支持多图上传
  • 图片管理界面

长期

  • 迁移到云存储
  • CDN 加速
  • 图片处理服务

优势

  1. 简单易用:不需要额外的云服务
  2. 成本低:只需服务器存储空间
  3. 完全控制:数据完全在自己手中
  4. 易于备份:直接备份文件夹即可
  5. 快速部署:无需配置云服务

⚠️ 限制

  1. 扩展性:单服务器存储容量有限
  2. 性能:大量访问时可能需要 CDN
  3. 备份:需要自己管理备份策略
  4. 多服务器:需要共享存储或同步

🎉 总结

成功实现了服务器文件系统存储方案,适合:

  • 自托管部署
  • 中小型应用
  • 对数据隐私有要求的场景
  • 不想依赖第三方云服务

如果未来需要更好的扩展性,可以轻松迁移到云存储服务!