javascript firebase express google-cloud-functions

javascript - Cómo realizar una carga de archivos HTTP usando express en Cloud Functions para Firebase(multer, busboy)



google-cloud-functions (7)

Estoy tratando de subir un archivo a Cloud Functions, usando Express para manejar las solicitudes allí, pero no estoy teniendo éxito. Creé una versión que funciona localmente:

servidor 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('', '')); });

cliente 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'' }, } );

Este mismo código parece romperse cuando se implementa en Cloud Functions, donde req.files no está definido. ¿Alguien tiene alguna idea de lo que está sucediendo aquí?

EDITAR También tuve la oportunidad de usar multer , que funcionó bien localmente, pero una vez cargado en Cloud Functions, esto me dio una matriz vacía (el mismo código del lado del cliente):

const app = express(); const upload = multer(); app.use(cors()); app.post(''/upload'', upload.any(), (req, res) => { res.send(JSON.stringify(req.files)); });


Arreglé algunos errores de la respuesta de G. Rodríguez. Añado el evento ''campo'' y ''finalizar'' para Busboy, y hago next () en el evento ''finalizar''. Esto es trabajo para mi. De la siguiente manera:

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() } })}


De hecho, hubo un cambio importante en la configuración de Cloud Functions que desencadenó este problema. Tiene que ver con la forma en que funciona el middleware que se aplica a todas las aplicaciones Express (incluida la aplicación predeterminada) que se utiliza para servir las funciones HTTPS. Básicamente, Cloud Functions analizará el cuerpo de la solicitud y decidirá qué hacer con ella, dejando el contenido sin procesar del cuerpo en un Buffer en req.rawBody . Puede usar esto para analizar directamente su contenido multiparte, pero no puede hacerlo con middleware (como multer).

En su lugar, puede usar un módulo llamado busboy para lidiar directamente con el contenido del cuerpo sin procesar. Puede aceptar el búfer rawBody y le devolverá la llamada con los archivos que encontró. Aquí hay un código de muestra que iterará todo el contenido cargado, los guardará como archivos y luego los eliminará. Obviamente querrás hacer algo más útil.

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(); } })

Tenga en cuenta que los archivos guardados en el espacio temporal ocupan memoria, por lo que sus tamaños deben limitarse a un total de 10 MB. Para archivos más grandes, debe cargarlos en Cloud Storage y procesarlos con un activador de almacenamiento.

También tenga en cuenta que la selección predeterminada de middleware agregada por Cloud Functions no se agrega actualmente al emulador local a través de firebase serve . Por lo tanto, esta muestra no funcionará (rawBody no estará disponible) en ese caso.

El equipo está trabajando en actualizar la documentación para tener una idea más clara de lo que sucede durante las solicitudes HTTPS que es diferente de una aplicación Express estándar.


Gracias a las respuestas anteriores , he construido un módulo npm para esto ( github )

Funciona con las funciones de la nube de Google, solo instálelo ( npm install --save express-multipart-file-parser ) y utilícelo así:

const fileMiddleware = require(''express-multipart-file-parser'') ... app.use(fileMiddleware) ... app.post(''/file'', (req, res) => { const { fieldname, filename, encoding, mimetype, buffer, } = req.files[0] ... })


Gracias por la ayuda de todos en este hilo. Perdí todo un día probando todas las combinaciones posibles y todas estas bibliotecas diferentes ... solo para descubrir esto después de agotar todas las demás opciones.

Combiné algunas de las soluciones anteriores para crear un script compatible con TypeScript y middleware aquí:

https://gist.github.com/jasonbyrne/8dcd15701f686a4703a72f13e3f800c0


He estado sufriendo el mismo problema durante unos días, resulta que el equipo de Firebase ha puesto el cuerpo sin procesar de multipart / form-data en req.body con su middleware. Si intenta console.log (req.body.toString ()) ANTES de procesar su solicitud con multer, verá sus datos. A medida que multer crea un nuevo objeto req.body que anula la req resultante, los datos desaparecen y todo lo que podemos obtener es un req.body vacío. Esperemos que el equipo de Firebase pueda corregir esto pronto.


Para agregar a la respuesta oficial del equipo de Cloud Function, puede emular este comportamiento localmente haciendo lo siguiente (agregue este middleware más alto que el código busboy que publicaron, obviamente)

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(); } });


Pude combinar la respuesta de Brian y de Doug. Aquí está mi middleware que termina imitando los archivos req en multer para que no haya cambios importantes en el resto de su código.

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() } })}