Как выполнить загрузку HTTP файлов с помощью функции экспресс-загрузки для Firebase (multer, busboy)
Я пытаюсь загрузить файл в Cloud Functions, используя Express для обработки запросов там, но я не преуспеваю. Я создал версию, которая работает локально:
serverside 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(', '));
});
clientside 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 - undefined. Кто-нибудь знает, что здесь происходит?
ИЗМЕНИТЬ
Я также пошел на использование multer
, который работал нормально локально, но после загрузки в Cloud Functions это достало мне пустой массив (тот же клиентский код):
const app = express();
const upload = multer();
app.use(cors());
app.post('/upload', upload.any(), (req, res) => {
res.send(JSON.stringify(req.files));
});
Ответы
Ответ 1
Действительно, в настройках облачных функций произошла резкая перестройка, которая вызвала эту проблему. Это связано с тем, как работает промежуточное программное обеспечение, которое применяется ко всем приложениям Express (включая приложение по умолчанию), используемым для обслуживания функций HTTPS. В принципе, функции облака будут анализировать тело запроса и решать, что с ним делать, оставляя исходное содержимое тела в буфере в req.rawBody
. Вы можете использовать это для непосредственного анализа вашего многостраничного контента, но вы не можете сделать это с помощью промежуточного программного обеспечения (например, multer).
Вместо этого вы можете использовать модуль busboy, чтобы напрямую обращаться к содержимому исходного содержимого. Он может принимать буфер 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 finished.
busboy.end(req.rawBody);
} else {
// Client error - only support POST
res.status(405).end();
}
})
Имейте в виду, что файлы, сохраненные в временном пространстве, занимают память, поэтому их размеры должны быть ограничены в общей сложности 10 МБ. Для больших файлов вы должны загрузить их в Cloud Storage и обработать их с помощью триггера хранения.
Также учтите, что набор промежуточного программного обеспечения по умолчанию, добавленный функциями облака, в настоящее время не добавляется в локальный эмулятор через firebase serve
. Таким образом, этот образец не будет работать (rawBody будет недоступен) в этом случае.
Команда работает над обновлением документации, чтобы получить более четкое представление о том, что происходит во время запросов HTTPS, отличных от стандартного приложения Express.
Ответ 2
Я смог объединить ответ Брайана и Дага. Здесь мое промежуточное программное обеспечение, которое в конечном итоге имитирует req.files в multer, поэтому никаких изменений в остальной части вашего кода не происходит.
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()
}
})}
Ответ 3
Благодаря ответам выше для этого я создал модуль npm (github)
Он работает с облачными функциями 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]
...
})
Ответ 4
Я страдаю от одной и той же проблемы в течение нескольких дней, оказывается, что команда firebase поместила исходное тело multipart/form-data в req.body со своим промежуточным программным обеспечением. Если вы попробуете console.log(req.body.toString()) ПЕРЕД обработкой вашего запроса с помощью multer, вы увидите свои данные. Поскольку multer создает новый объект req.body, который переопределяет полученный req, данные уходят, и все, что мы можем получить, это пустой req.body. Надеюсь, команда firebase может исправить это в ближайшее время.
Ответ 5
Чтобы добавить к официальному ответу команды Cloud Function, вы можете эмулировать это поведение локально, выполнив следующее (добавьте это промежуточное ПО выше, чем код, который они размещают, очевидно)
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();
}
});
Ответ 6
Я исправил некоторые ошибки G. Родригес. Я добавляю событие "field" и "finish" для Busboy и выполняю next() в событии "finish". Это работа для меня. Как следует:
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()
}
})}
Ответ 7
Спасибо всем за помощь в этой теме. Я потратил целый день на всевозможные комбинации и все эти разные библиотеки... только для того, чтобы обнаружить это после исчерпания всех других вариантов.
Комбинируйте некоторые из вышеперечисленных решений для создания скриптов с поддержкой TypeScript и промежуточного ПО:
https://gist.github.com/jasonbyrne/8dcd15701f686a4703a72f13e3f800c0
Ответ 8
Обратите внимание, что помимо использования Busboy на сервере и анализа rawReq
, вам также может понадобиться добавить следующую конфигурацию в ваш запрос Axios:
{ headers: { 'content-type': 'multipart/form-data; boundary=${formData._boundary}' }};
Если вы указываете только content-type
, а не границу, вы получаете ошибку Boundary not found
на сервере. Если вы полностью удалите заголовки, вместо этого Busboy не будет правильно анализировать поля.
Смотрите: Облачные функции Firebase и Busboy не анализируют поля или файлы
Ответ 9
Я столкнулся с той же проблемой, когда развернул свое приложение, используя функцию firebase. Я использовал Multer для загрузки изображения на Amazon S3. Я решаю эту проблему, используя приведенный выше npm fooobar.com/info/2434282/..., созданный 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()
})
Обратите внимание, что это для загрузки одного файла изображения.
Следующее промежуточное ПО будет иметь возвращенный объект из s3
{
ETag: '"cacd6d406f891e216f9946911a69aac5"',
Location:'https://react-significant.s3.us-west1.amazonaws.com/posts/1567282665593',
key: 'posts/1567282665593',
Key: 'posts/1567282665593',
Bucket: 'react-significant'
}
В этом случае вам может понадобиться URL-адрес местоположения перед сохранением данных в БД.