一、为什么需要文件上传到云端?

在日常开发中,我们经常遇到需要让用户上传文件的场景。比如用户头像、文档附件、或者视频内容。如果直接把文件存在自己的服务器上,不仅会占用大量硬盘空间,还会增加服务器负担。这时候,对象存储服务(比如华为云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}`);
});

这段代码做了几件事:

  1. 创建了一个Express应用
  2. 使用multer中间件处理文件上传
  3. 设置了一个/upload接口接收单个文件
  4. 文件会暂存在服务器的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: '上传失败' });
    }
});

关键点说明:

  1. 创建OBS客户端需要AK/SK(在华为云控制台获取)
  2. 我们给每个文件生成唯一路径,避免重名覆盖
  3. 上传完成后删除本地临时文件
  4. 返回给前端可直接访问的URL

四、优化存储路径配置

直接往OBS根目录扔文件会很难管理,我们需要合理的路径规划。这里有几个实用技巧:

  1. 按日期分目录:
function getDatePath() {
    const now = new Date();
    return `${now.getFullYear()}/${now.getMonth()+1}/${now.getDate()}/`;
}
  1. 按用户隔离:
// 假设请求头带了用户ID
const userId = req.headers['x-user-id'] || 'anonymous';
const userPath = `users/${userId}/`;
  1. 按文件类型分类:
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: '上传失败' });
    }
});

六、安全注意事项

文件上传功能需要特别注意安全问题:

  1. 文件类型检查:
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
if (!ALLOWED_TYPES.includes(file.mimetype)) {
    throw new Error('不支持的文件类型');
}
  1. 文件大小限制:
const upload = multer({
    dest: 'uploads/',
    limits: { fileSize: 10 * 1024 * 1024 } // 10MB
});
  1. 病毒扫描:
// 可以调用第三方扫描服务
const isSafe = await virusScanService.scanFile(file.path);
if (!isSafe) {
    throw new Error('文件可能包含病毒');
}
  1. 权限控制:
// 确保用户只能访问自己的文件
app.get('/download/:fileKey', (req, res) => {
    const userPath = `users/${req.user.id}/`;
    if (!req.params.fileKey.startsWith(userPath)) {
        return res.status(403).send('无权访问');
    }
    // ...处理下载
});

七、实际应用场景

这种技术方案特别适合以下场景:

  1. 用户生成内容平台:比如博客网站的图片上传、视频分享平台的视频上传。

  2. 企业文档管理系统:员工可以上传各类工作文档,自动分类存储。

  3. 电商平台:商家批量上传商品图片和描述文件。

  4. 在线教育平台:学生提交作业,老师上传课件。

  5. 社交应用:用户上传头像和分享图片。

八、技术方案优缺点

优点:

  • 节省服务器存储空间
  • 扩展性强,存储量几乎无限
  • 专业团队维护,可靠性高
  • 自带CDN加速,访问速度快
  • 按量付费,成本可控

缺点:

  • 需要额外学习云存储API
  • 产生少量外网流量费用
  • 依赖第三方服务可用性

九、常见问题解决方案

  1. 上传速度慢:
  • 检查客户端到OBS的区域是否匹配
  • 启用传输加速功能
  • 对大文件使用分片上传
  1. 权限问题:
  • 确保AK/SK有足够权限
  • 检查桶的读写权限设置
  • 临时权限可以使用临时AK/SK
  1. 文件覆盖:
  • 使用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('服务运行中...'));

总结一下关键点:

  1. 使用OBS可以极大减轻服务器存储压力
  2. 合理的路径规划让文件管理更轻松
  3. 注意文件上传的安全限制
  4. 大文件要使用分片上传
  5. 完善的错误处理提升用户体验

希望这篇文章能帮助你快速实现文件上传到云端的功能。如果遇到问题,可以查阅华为云OBS的官方文档,或者加入开发者社区讨论。