一、为什么需要文件上传到云端?
在日常开发中,我们经常遇到需要让用户上传文件的场景。比如用户头像、文档附件、或者视频内容。如果直接把文件存在自己的服务器上,不仅会占用大量硬盘空间,还会增加服务器负担。这时候,对象存储服务(比如华为云OBS)就成了更好的选择。
想象一下,你开了一家网店,顾客上传的商品图片如果全堆在自家仓库里,很快仓库就不够用了。而OBS就像租用了一个无限大的云仓库,按实际使用量付费,还能自动帮你管理文件。
二、搭建基础Express服务
首先我们需要一个能接收文件的Node.js服务。Express是最常用的选择,它就像快递公司的前台,负责接收包裹(文件)并登记信息。
// 技术栈:Node.js + Express
const express = require('express');
const multer = require('multer'); // 处理文件上传的中间件
const path = require('path');
const app = express();
const port = 3000;
// 临时存储上传的文件
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
// req.file 是上传的文件信息
console.log('收到文件:', req.file);
res.send('文件上传成功!');
});
app.listen(port, () => {
console.log(`服务已启动,访问 http://localhost:${port}`);
});
这段代码做了几件事:
- 创建了一个Express应用
- 使用multer中间件处理文件上传
- 设置了一个/upload接口接收单个文件
- 文件会暂存在服务器的uploads文件夹
三、集成OBS SDK实现云端存储
现在我们要把收到的文件转存到OBS。首先需要安装官方SDK:
npm install @huaweicloud/huaweicloud-sdk-obs
然后改造我们的上传接口:
// 技术栈:Node.js + Express + OBS SDK
const obs = require('@huaweicloud/huaweicloud-sdk-obs');
// 初始化OBS客户端
const obsClient = new obs.ObsClient({
access_key_id: '你的AK',
secret_access_key: '你的SK',
server: 'https://your-endpoint'
});
app.post('/upload-to-obs', upload.single('file'), async (req, res) => {
try {
const file = req.file;
const fileKey = `user-uploads/${Date.now()}-${file.originalname}`;
// 上传到OBS
const result = await obsClient.putObject({
Bucket: '你的桶名',
Key: fileKey,
Body: require('fs').createReadStream(file.path)
});
// 删除临时文件
require('fs').unlinkSync(file.path);
res.json({
code: 0,
data: {
url: `https://your-bucket.obs.your-region.myhuaweicloud.com/${fileKey}`
}
});
} catch (err) {
console.error('上传失败:', err);
res.status(500).json({ code: -1, message: '上传失败' });
}
});
关键点说明:
- 创建OBS客户端需要AK/SK(在华为云控制台获取)
- 我们给每个文件生成唯一路径,避免重名覆盖
- 上传完成后删除本地临时文件
- 返回给前端可直接访问的URL
四、优化存储路径配置
直接往OBS根目录扔文件会很难管理,我们需要合理的路径规划。这里有几个实用技巧:
- 按日期分目录:
function getDatePath() {
const now = new Date();
return `${now.getFullYear()}/${now.getMonth()+1}/${now.getDate()}/`;
}
- 按用户隔离:
// 假设请求头带了用户ID
const userId = req.headers['x-user-id'] || 'anonymous';
const userPath = `users/${userId}/`;
- 按文件类型分类:
const ext = path.extname(file.originalname).slice(1) || 'other';
const typePath = `${ext}/`;
最终路径组合示例:
const fileKey = `${getDatePath()}${userPath}${typePath}${file.originalname}`;
这样上传的文件会按"年/月/日/用户ID/文件类型/原始文件名"的层次存储,既清晰又便于后续管理。
五、处理大文件上传
当遇到视频等大文件时,直接上传可能会超时。OBS支持分片上传,我们可以这样实现:
app.post('/big-file-upload', upload.single('file'), async (req, res) => {
try {
const file = req.file;
const fileKey = `big-files/${file.originalname}`;
// 初始化分片上传
const initResult = await obsClient.initiateMultipartUpload({
Bucket: '你的桶名',
Key: fileKey
});
const uploadId = initResult.UploadId;
const partSize = 5 * 1024 * 1024; // 5MB每片
const fileStats = require('fs').statSync(file.path);
const parts = [];
// 分片上传
let partNumber = 1;
for (let start = 0; start < fileStats.size; start += partSize) {
const end = Math.min(start + partSize, fileStats.size);
const partStream = require('fs').createReadStream(
file.path, { start, end }
);
const partResult = await obsClient.uploadPart({
Bucket: '你的桶名',
Key: fileKey,
PartNumber: partNumber,
UploadId: uploadId,
Body: partStream
});
parts.push({
PartNumber: partNumber,
ETag: partResult.ETag
});
partNumber++;
}
// 完成分片上传
await obsClient.completeMultipartUpload({
Bucket: '你的桶名',
Key: fileKey,
UploadId: uploadId,
Parts: parts
});
require('fs').unlinkSync(file.path);
res.json({ code: 0, data: { url: fileKey } });
} catch (err) {
console.error('分片上传失败:', err);
res.status(500).json({ code: -1, message: '上传失败' });
}
});
六、安全注意事项
文件上传功能需要特别注意安全问题:
- 文件类型检查:
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new Error('不支持的文件类型');
}
- 文件大小限制:
const upload = multer({
dest: 'uploads/',
limits: { fileSize: 10 * 1024 * 1024 } // 10MB
});
- 病毒扫描:
// 可以调用第三方扫描服务
const isSafe = await virusScanService.scanFile(file.path);
if (!isSafe) {
throw new Error('文件可能包含病毒');
}
- 权限控制:
// 确保用户只能访问自己的文件
app.get('/download/:fileKey', (req, res) => {
const userPath = `users/${req.user.id}/`;
if (!req.params.fileKey.startsWith(userPath)) {
return res.status(403).send('无权访问');
}
// ...处理下载
});
七、实际应用场景
这种技术方案特别适合以下场景:
用户生成内容平台:比如博客网站的图片上传、视频分享平台的视频上传。
企业文档管理系统:员工可以上传各类工作文档,自动分类存储。
电商平台:商家批量上传商品图片和描述文件。
在线教育平台:学生提交作业,老师上传课件。
社交应用:用户上传头像和分享图片。
八、技术方案优缺点
优点:
- 节省服务器存储空间
- 扩展性强,存储量几乎无限
- 专业团队维护,可靠性高
- 自带CDN加速,访问速度快
- 按量付费,成本可控
缺点:
- 需要额外学习云存储API
- 产生少量外网流量费用
- 依赖第三方服务可用性
九、常见问题解决方案
- 上传速度慢:
- 检查客户端到OBS的区域是否匹配
- 启用传输加速功能
- 对大文件使用分片上传
- 权限问题:
- 确保AK/SK有足够权限
- 检查桶的读写权限设置
- 临时权限可以使用临时AK/SK
- 文件覆盖:
- 使用UUID等唯一文件名
- 先检查文件是否存在
- 启用版本控制功能
十、完整示例与总结
最后给一个完整的最佳实践示例:
// 技术栈:Node.js + Express + OBS SDK
const express = require('express');
const multer = require('multer');
const path = require('path');
const obs = require('@huaweicloud/huaweicloud-sdk-obs');
const { v4: uuidv4 } = require('uuid');
const app = express();
const upload = multer({
dest: 'tmp/',
limits: { fileSize: 100 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'application/pdf'];
cb(null, allowed.includes(file.mimetype));
}
});
const obsClient = new obs.ObsClient({
access_key_id: process.env.OBS_AK,
secret_access_key: process.env.OBS_SK,
server: process.env.OBS_ENDPOINT
});
// 智能路径生成
function generateFileKey(userId, originalname) {
const now = new Date();
const datePath = `${now.getFullYear()}/${now.getMonth()+1}/${now.getDate()}`;
const ext = path.extname(originalname).slice(1) || 'other';
const uniqueName = `${uuidv4()}${path.extname(originalname)}`;
return `uploads/${datePath}/users/${userId}/${ext}/${uniqueName}`;
}
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
const userId = req.user.id; // 从认证信息获取
const fileKey = generateFileKey(userId, req.file.originalname);
await obsClient.putObject({
Bucket: process.env.OBS_BUCKET,
Key: fileKey,
Body: require('fs').createReadStream(req.file.path)
});
require('fs').unlinkSync(req.file.path);
res.json({
success: true,
url: `https://${process.env.OBS_BUCKET}.${process.env.OBS_ENDPOINT}/${fileKey}`
});
} catch (err) {
console.error(err);
res.status(500).json({ success: false, error: '上传失败' });
}
});
app.listen(3000, () => console.log('服务运行中...'));
总结一下关键点:
- 使用OBS可以极大减轻服务器存储压力
- 合理的路径规划让文件管理更轻松
- 注意文件上传的安全限制
- 大文件要使用分片上传
- 完善的错误处理提升用户体验
希望这篇文章能帮助你快速实现文件上传到云端的功能。如果遇到问题,可以查阅华为云OBS的官方文档,或者加入开发者社区讨论。
评论