web-dev-qa-db-fra.com

Comment effectuer un téléchargement de fichier HTTP en utilisant Express on Cloud Functions for Firebase (multer, busboy)

J'essaie de télécharger un fichier dans Cloud Functions, en utilisant Express pour gérer les demandes, mais je n'y parviens pas. J'ai créé une version qui fonctionne localement:

js côté serveur

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

côté client 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' },
    }
);

Ce même code semble rompre lorsqu'il est déployé dans Cloud Functions, où req.files n'est pas défini. Quelqu'un a-t-il une idée de ce qui se passe ici?

EDIT J'ai aussi essayé d'utiliser multer, qui fonctionnait bien localement, mais une fois téléchargé sur Cloud Functions, cela m'a donné un tableau vide (même code côté client):

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

app.post('/upload', upload.any(), (req, res) => {
    res.send(JSON.stringify(req.files));
});
23
Eindbaas

Il y a eu en effet un changement de rupture dans la configuration des fonctions cloud qui a déclenché ce problème. Cela a à voir avec le fonctionnement du middleware appliqué à toutes les applications Express (y compris l'application par défaut) utilisées pour servir les fonctions HTTPS. Fondamentalement, Cloud Functions analysera le corps de la demande et décidera quoi en faire, en laissant le contenu brut du corps dans un tampon dans req.rawBody. Vous pouvez l'utiliser pour analyser directement votre contenu en plusieurs parties, mais vous ne pouvez pas le faire avec un middleware (comme multer).

Au lieu de cela, vous pouvez utiliser un module appelé busboy pour traiter directement le contenu du corps brut. Il peut accepter le tampon rawBody et vous rappellera avec les fichiers qu'il a trouvés. Voici un exemple de code qui itérera tout le contenu téléchargé, les enregistrera en tant que fichiers, puis les supprimera. Vous voudrez évidemment faire quelque chose de plus utile.

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

Gardez à l'esprit que les fichiers enregistrés dans l'espace temporaire occupent de la mémoire, leur taille doit donc être limitée à un total de 10 Mo. Pour les fichiers plus volumineux, vous devez les télécharger sur Cloud Storage et les traiter avec un déclencheur de stockage.

Gardez également à l'esprit que la sélection par défaut de middleware ajoutée par Cloud Functions n'est pas actuellement ajoutée à l'émulateur local via firebase serve. Cet exemple ne fonctionnera donc pas (rawBody ne sera pas disponible) dans ce cas.

L'équipe travaille sur la mise à jour de la documentation pour être plus clair sur ce qui se passe lors des requêtes HTTPS, différent d'une application Express standard.

34
Doug Stevenson

J'ai pu combiner la réponse de Brian et de Doug. Voici mon middleware qui finit par imiter les fichiers de demande dans multer, donc pas de changements de rupture dans le reste de votre code.

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()
    }
})}
12
G. Rodriguez

Merci au réponses ci-dessus J'ai construit un module npm pour cela ( github )

Il fonctionne avec les fonctions de Google Cloud, installez-le simplement (npm install --save express-multipart-file-parser) et utilisez-le comme ceci:

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

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

app.post('/file', (req, res) => {
  const {
    fieldname,
    filename,
    encoding,
    mimetype,
    buffer,
  } = req.files[0]
  ...
})
10

Je souffre du même problème depuis quelques jours, il s'avère que l'équipe Firebase a mis le corps brut des données en plusieurs parties/formulaires dans req.body avec leur middleware. Si vous essayez console.log (req.body.toString ()) AVANT de traiter votre demande avec multer, vous verrez vos données. Comme multer crée un nouvel objet req.body qui remplace la demande résultante, les données ont disparu et tout ce que nous pouvons obtenir est un corps de demande vide. J'espère que l'équipe Firebase pourra bientôt corriger cela.

7
James Kuang

Pour ajouter à la réponse officielle de l'équipe Cloud Function, vous pouvez émuler ce comportement localement en procédant comme suit (ajoutez ce middleware plus haut que le code busboy qu'ils ont publié, évidemment)

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

J'ai corrigé quelques bugs de la réponse de G. Rodriguez. J'ajoute l'événement 'field' et 'finish' pour Busboy, et je fais ensuite () dans l'événement 'finish'. C'est du travail pour moi. Comme suit:

    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()
        }
    })}
3
Shuangquan Wei

Merci pour l'aide de tout le monde sur ce fil. J'ai perdu une journée entière à essayer toutes les combinaisons possibles et toutes ces différentes bibliothèques ... pour découvrir cela après avoir épuisé toutes les autres options.

Combiné certaines des solutions ci-dessus pour créer un script compatible avec TypeScript et middleware ici:

https://Gist.github.com/jasonbyrne/8dcd15701f686a4703a72f13e3f800c

3
Jason Byrne

Notez que, en plus d'utiliser Busboy sur le serveur et d'analyser le rawReq, vous devrez peut-être également ajouter la configuration suivante à votre demande Axios:

{ headers: { 'content-type': `multipart/form-data; boundary=${formData._boundary}` }};

Si vous spécifiez uniquement le content-type et non la limite, vous obtenez un Boundary not found erreur sur le serveur. Si vous supprimez complètement les en-têtes, à la place, Busboy n'analysera pas correctement les champs. Voir: Firebase Cloud Functions et Busboy ne pas analyser les champs ou les fichiers

0
Bruno

Je rencontre le même problème lorsque j'ai déployé mon application à l'aide de la fonction Firebase. J'utilisais multer pour télécharger une image sur Amazon s3. Je résout ce problème en utilisant le npm ci-dessus https://stackoverflow.com/a/48648805/521379 créé par Cristóvão.

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

Notez qu'il s'agit d'un téléchargement d'image de fichier unique. Le prochain middleware aura l'objet retourné de s3

{ 
  ETag: '"cacd6d406f891e216f9946911a69aac5"',
  Location:'https://react-significant.s3.us-west1.amazonaws.com/posts/1567282665593',
  key: 'posts/1567282665593',
  Key: 'posts/1567282665593',
  Bucket: 'react-significant' 
}

Dans ce cas, vous pourriez avoir besoin de l'URL de localisation avant d'enregistrer vos données dans la base de données.

0
mrvncaragay