Javascript 如何使用Firebase(multer、busboy)的云上express功能执行HTTP文件上载
我试图上传一个文件到云函数,使用Express处理那里的请求,但我没有成功。我创建了一个在本地工作的版本: 服务器端jsJavascript 如何使用Firebase(multer、busboy)的云上express功能执行HTTP文件上载,javascript,firebase,express,google-cloud-functions,Javascript,Firebase,Express,Google 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
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(', '));
});
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' },
}
);
客户端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(', '));
});
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' },
}
);
当部署到未定义req.files的云函数时,这个完全相同的代码似乎会中断。有人知道这里发生了什么吗
编辑
我还尝试过使用multer
,它在本地运行得很好,但一旦上传到云函数,就得到了一个空数组(相同的客户端代码):
几天来,我一直在遭受同样的问题,事实证明firebase团队已经将多部分/表单数据的原始主体与他们的中间件一起放入req.body中。如果在使用multer处理请求之前尝试console.log(req.body.toString()),您将看到数据。当multer创建一个新的req.body对象覆盖生成的req时,数据就消失了,我们只能得到一个空的req.body。希望firebase团队能够很快纠正这一问题。云功能设置中确实发生了突破性的变化,引发了这一问题。这与中间件的工作方式有关,中间件应用于所有用于提供HTTPS功能的Express应用程序(包括默认应用程序)。基本上,云函数将解析请求的主体并决定如何处理它,将主体的原始内容保留在
req.rawBody
中的缓冲区中。您可以使用它直接解析您的多部分内容,但不能使用中间件(如multer)
相反,您可以使用一个名为的模块直接处理原始正文内容。它可以接受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。对于较大的文件,您应该将其上载到云存储,并使用存储触发器对其进行处理
还请记住,云函数添加的中间件的默认选择当前未通过firebase Service
添加到本地模拟器。因此,在这种情况下,此示例将不起作用(rawBody将不可用)
团队正在更新文档,以便更清楚地了解HTTPS请求过程中与标准Express应用程序不同的情况。要添加到官方云功能团队答案,您可以通过执行以下操作在本地模拟此行为(显然,将此中间件添加到比他们发布的busboy代码更高的位置)
我能够将Brian和Doug的响应结合起来。这是我的中间件,它最终模拟了multer中的req.文件,因此不会对代码的其余部分进行破坏性更改
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()
}
})}
我修复了G.Rodriguez的响应中的一些错误。我为Busboy添加了'field'和'finish'事件,并在'finish'事件中执行next()。这是我的工作。如下所示:
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('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('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
console.log('Field [' + fieldname + ']: value: ' + inspect(val));
});
busboy.on('finish', function() {
next()
});
busboy.end(req.rawBody)
req.pipe(busboy);
} else {
next()
}
})}
多亏了我为这个()构建了一个npm模块
它与google云功能配合使用,只需安装它(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]
...
})
谢谢大家在这方面的帮助。我浪费了一整天的时间尝试各种可能的组合和所有这些不同的库…在用尽所有其他选择后才发现这一点 结合上述一些解决方案,在此处创建一个TypeScript和支持中间件的脚本:
请注意,除了在服务器上使用Busboy并解析
rawReq
之外,您可能还需要在Axios请求中添加以下配置:
{ headers: { 'content-type': `multipart/form-data; boundary=${formData._boundary}` }};
如果只指定内容类型
而不指定边界,则服务器上会出现边界未找到
错误。如果完全删除标题,Busboy将无法正确解析字段。
请参阅:当我使用firebase功能部署我的应用程序时,我遇到了同样的问题。我使用multer将图像上传到amazon s3。我使用Cristóvão创建的上述npm解决了这个问题
const { mimetype, buffer, } = req.files[0]
let s3bucket = new aws.S3({
accessKeyId: functions.config().aws.access_key,
secretAccessKey: functions.config().aws.secret_key,
});
const config = {
Bucket: functions.config().aws.bucket_name,
ContentType: mimetype,
ACL: 'public-read',
Key: Date.now().toString(),
Body: buffer,
}
s3bucket.upload(config, (err, data) => {
if(err) console.log(err)
req.file = data;
next()
})
请注意,这是针对单个文件图像上载的。
下一个中间件将从s3返回对象
{
ETag: '"cacd6d406f891e216f9946911a69aac5"',
Location:'https://react-significant.s3.us-west1.amazonaws.com/posts/1567282665593',
key: 'posts/1567282665593',
Key: 'posts/1567282665593',
Bucket: 'react-significant'
}
在这种情况下,在将数据保存到数据库之前,您可能需要位置url。云函数在进一步传递前预处理
请求
对象。因此,原始的multer
中间件不起作用。此外,使用busboy
的级别太低,您需要处理上的一切您自己的,这并不理想。相反,您可以使用中间件处理云函数上的多部分/表单数据
这是你能做的
安装拨叉
npm安装--保存emadalam/multer#master
使用startProcessing
配置自定义处理云函数添加的req.rawBody
const express=require('express'))
常量multer=require('multer')
常量大小限制=10*1024*1024//10MB
const app=express()
const multipartFormDataParser=multer({
存储:multer.memoryStorage(),
//如果需要,增加尺寸限制
限制:{fieldSize:SIZE\u LIMIT},
//支持firebase云功能
//多部分表单数据请求对象由云函数预处理
//目前,“multer”库本机不支持这种行为
//因此,通过添加'startProcessing'来维护自定义fork以启用此功能`
// https://github.com/emadalam/multer
启动处理(请求,服务生){
所需原料体?汇流排末端(所需原料体):所需管道(汇流排)
},
})
app.post('/some_route',multipartFormDataParser.any(),函数(req,res,next){
//req.files是上传文件的数组
//请求机构将公司
// multiparts.js
const { createWriteStream } = require('fs')
const { tmpdir } = require('os')
const { join } = require('path')
const BusBoy = require('busboy')
exports.extractFiles = async(req, res, next) => {
const multipart = req.method === 'POST' && req.headers['content-type'].startsWith('multipart/form-data')
if (!multipart) return next()
//
const busboy = new BusBoy({ headers: req.headers })
const incomingFields = {}
const incomingFiles = {}
const writes = []
// Process fields
busboy.on('field', (name, value) => {
try {
// This will keep a field created like so form.append('product', JSON.stringify(product)) intact
incomingFields[name] = JSON.parse(value)
} catch (e) {
// Numbers will still be strings here (i.e 1 will be '1')
incomingFields[name] = value
}
})
// Process files
busboy.on('file', (field, file, filename, encoding, contentType) => {
// Doing this to not have to deal with duplicate file names
// (i.e. TIMESTAMP-originalName. Hmm what are the odds that I'll still have dups?)
const path = join(tmpdir(), `${(new Date()).toISOString()}-${filename}`)
// NOTE: Multiple files could have same fieldname (which is y I'm using arrays here)
incomingFiles[field] = incomingFiles[field] || []
incomingFiles[field].push({ path, encoding, contentType })
//
const writeStream = createWriteStream(path)
//
writes.push(new Promise((resolve, reject) => {
file.on('end', () => { writeStream.end() })
writeStream.on('finish', resolve)
writeStream.on('error', reject)
}))
//
file.pipe(writeStream)
})
//
busboy.on('finish', async () => {
await Promise.all(writes)
req.files = incomingFiles
req.body = incomingFields
next()
})
busboy.end(req.rawBody)
}
// index.js
const { onRequest } = require('firebase-functions').https
const bodyParser = require('body-parser')
const express = require('express')
const cors = require('cors')
const app = express()
// First middleware I'm adding
const { extractFiles } = require('./multiparts')
app.use(extractFiles)
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use(cors({ origin: true }))
app.use((req) => console.log(req.originalUrl))
exports.MyFunction = onRequest(app);