如何在 Cloud Functions for Firebase 上使用 express 执行 HTTP 文件上传(multer、busboy)

IT技术 javascript firebase express google-cloud-functions
2021-02-10 20:11:43

我正在尝试将文件上传到 Cloud Functions,使用 Express 处理那里的请求,但我没有成功。我创建了一个在本地工作的版本:

服务器端js

const express = require('express');
const cors = require('cors');
const fileUpload = require('express-fileupload');

const app = express();
app.use(fileUpload());
app.use(cors());

app.post('/upload', (req, res) => {
    res.send('files: ' + Object.keys(req.files).join(', '));
});

客户端js

const formData = new FormData();
Array.from(this.$refs.fileSelect.files).forEach((file, index) => {
    formData.append('sample' + index, file, 'sample');
});

axios.post(
    url,
    formData, 
    {
        headers: { 'Content-Type': 'multipart/form-data' },
    }
);

这个完全相同的代码在部署到 Cloud Functions 时似乎会中断,其中 req.files 未定义。有谁知道这里发生了什么?

编辑 我也multer尝试了 using ,它在本地运行良好,但是一旦上传到 Cloud Functions,这给了我一个空数组(相同的客户端代码):

const app = express();
const upload = multer();
app.use(cors());

app.post('/upload', upload.any(), (req, res) => {
    res.send(JSON.stringify(req.files));
});
6个回答

Cloud Functions 设置确实发生了重大变化,触发了此问题。它与中间件的工作方式有关,该方式应用于所有用于提供 HTTPS 功能的 Express 应用程序(包括默认应用程序)。基本上,Cloud Functions 将解析请求的正文并决定如何处理它,将正文的原始内容留在req.rawBody. 您可以使用它来直接解析您的多部分内容,但您不能使用中间件(如 multer)来做到这一点。

相反,您可以使用名为busboy的module直接处理原始正文内容。它可以接受rawBody缓冲区并用它找到的文件给你回电。下面是一些示例代码,它将迭代所有上传的内容,将它们保存为文件,然后删除它们。您显然会想做一些更有用的事情。

const path = require('path');
const os = require('os');
const fs = require('fs');
const Busboy = require('busboy');

exports.upload = functions.https.onRequest((req, res) => {
    if (req.method === 'POST') {
        const busboy = new Busboy({ headers: req.headers });
        // This object will accumulate all the uploaded files, keyed by their name
        const uploads = {}

        // This callback will be invoked for each file uploaded
        busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
            console.log(`File [${fieldname}] filename: ${filename}, encoding: ${encoding}, mimetype: ${mimetype}`);
            // Note that os.tmpdir() is an in-memory file system, so should only 
            // be used for files small enough to fit in memory.
            const filepath = path.join(os.tmpdir(), fieldname);
            uploads[fieldname] = { file: filepath }
            console.log(`Saving '${fieldname}' to ${filepath}`);
            file.pipe(fs.createWriteStream(filepath));
        });

        // This callback will be invoked after all uploaded files are saved.
        busboy.on('finish', () => {
            for (const name in uploads) {
                const upload = uploads[name];
                const file = upload.file;
                res.write(`${file}\n`);
                fs.unlinkSync(file);
            }
            res.end();
        });

        // The raw bytes of the upload will be in req.rawBody.  Send it to busboy, and get
        // a callback when it's finished.
        busboy.end(req.rawBody);
    } else {
        // Client error - only support POST
        res.status(405).end();
    }
})

请记住,保存到临时空间的文件会占用内存,因此它们的总大小应限制为 10MB。对于较大的文件,您应该将它们上传到 Cloud Storage 并使用存储触发器处理它们。

另请记住,由 Cloud Functions 添加的默认中间件选择当前未通过firebase serve. 因此,在这种情况下,此示例将不起作用(rawBody 将不可用)。

该团队正在更新文档,以更清楚地了解与标准 Express 应用程序不同的 HTTPS 请求期间发生的一切。

我刚刚尝试过这个解决方案(由谷歌推荐),但它对我不起作用。它对我不起作用。结果如下:客户端(Chrome,fetch)将对本地 Firebase 服务器的调用记录为“multipart/form-data”,其中包含我要上传的文件和一个字段。Firebase 服务器没有显示对“busboy.on('file'...)”的任何调用。有一个对“busboy.on('finish', ...)”的调用,但是“uploads”数组是空的。
2021-03-14 20:11:43
我已经尝试过您的解决方案,但我正在尝试使用 busboy 的“字段”回调来收集正文内容。但似乎field事件从未被触发,然后finish也从未被触发,并且请求最终超时。
2021-03-17 20:11:43
谢谢你的回答。这个小问题花了我 2 天时间。
2021-03-29 20:11:43

感谢上面答案,我为此构建了一个 npm module(github

它适用于谷歌云功能,只需安装它 ( npm install --save express-multipart-file-parser) 并像这样使用它:

const fileMiddleware = require('express-multipart-file-parser')

...
app.use(fileMiddleware)
...

app.post('/file', (req, res) => {
  const {
    fieldname,
    filename,
    encoding,
    mimetype,
    buffer,
  } = req.files[0]
  ...
})
@S.Bozzoni,究竟是什么??
2021-03-18 20:11:43
虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会无效
2021-03-24 20:11:43
你能在你的 github rpo 上提供更多的例子吗?
2021-03-24 20:11:43
一些使用示例,我寻找了它,但我不是节点专家。与 busboy 相比,这看起来真的很容易,但我会很欣赏一些例子,比如多个文件的处理和保存。仅适用于节点新手。谢谢你。
2021-03-28 20:11:43
添加了代码示例,只保留链接作为参考
2021-04-02 20:11:43

我能够结合布赖恩和道格的回应。这是我的中间件,它最终模仿了 multer 中的 req.files,因此不会对其余代码进行重大更改。

module.exports = (path, app) => {
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use((req, res, next) => {
    if(req.rawBody === undefined && req.method === 'POST' && req.headers['content-type'].startsWith('multipart/form-data')){
        getRawBody(req, {
            length: req.headers['content-length'],
            limit: '10mb',
            encoding: contentType.parse(req).parameters.charset
        }, function(err, string){
            if (err) return next(err)
            req.rawBody = string
            next()
        })
    } else {
        next()
    }
})

app.use((req, res, next) => {
    if (req.method === 'POST' && req.headers['content-type'].startsWith('multipart/form-data')) {
        const busboy = new Busboy({ headers: req.headers })
        let fileBuffer = new Buffer('')
        req.files = {
            file: []
        }

        busboy.on('field', (fieldname, value) => {
            req.body[fieldname] = value
        })

        busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
            file.on('data', (data) => {
                fileBuffer = Buffer.concat([fileBuffer, data])
            })

            file.on('end', () => {
                const file_object = {
                    fieldname,
                    'originalname': filename,
                    encoding,
                    mimetype,
                    buffer: fileBuffer
                }

                req.files.file.push(file_object)
            })
        })

        busboy.on('finish', () => {
            next()
        })


        busboy.end(req.rawBody)
        req.pipe(busboy)
    } else {
        next()
    }
})}
谢谢你,我已经把它发布到 npm,检查我的答案如下
2021-03-18 20:11:43
此方法丢弃由多部分形式传递的所有字符串属性
2021-03-20 20:11:43
添加此代码会删除我所有的其他云功能。还只是想知道,启用此功能后,帖子会发布到哪个 URL 路径?
2021-03-23 20:11:43
非常感谢这个答案!真的很感激。
2021-03-31 20:11:43
是的,刚刚为示例添加了一个修复程序。它基本上添加了 busboy.on('field') 并将其保存到 OG req.body。感谢您的反馈!@rendom
2021-04-04 20:11:43

我已经被同样的问题困扰了几天,结果是 firebase 团队已经将 multipart/form-data 的原始主体和他们的中间件一起放入 req.body 中。如果您在使用 multer 处理您的请求之前尝试 console.log(req.body.toString()) ,您将看到您的数据。由于 multer 创建了一个新的 req.body 对象,它覆盖了生成的 req,数据消失了,我们只能得到一个空的 req.body。希望 firebase 团队能尽快纠正这个问题。

有关更多详细信息,请参阅我的答案。
2021-04-10 20:11:43

要添加到 Cloud Function 团队的官方答案中,您可以通过执行以下操作在本地模拟此行为(显然,将此中间件添加到比他们发布的 busboy 代码更高的位置)

const getRawBody = require('raw-body');
const contentType = require('content-type');

app.use(function(req, res, next){
    if(req.rawBody === undefined && req.method === 'POST' && req.headers['content-type'] !== undefined && req.headers['content-type'].startsWith('multipart/form-data')){
        getRawBody(req, {
            length: req.headers['content-length'],
            limit: '10mb',
            encoding: contentType.parse(req).parameters.charset
        }, function(err, string){
            if (err) return next(err);
            req.rawBody = string;
            next();
        });
    }
    else{
        next();
    }
});