Поток загрузки файла на S3 на Node.js с использованием грозного и (knox или aws-sdk)
Я пытаюсь передать файл, отправленный через форму непосредственно в ведро Amazon S3, используя aws-sdk или knox. Обработка формы выполняется с помощью formidable.
Мой вопрос: как я могу правильно использовать aws-sdk (или knox), используя новейшие функции каждой из этих библиотек для обработки потоков?
Я знаю, что эта тема уже задана здесь в разных вариантах, например:
Тем не менее, я считаю, что ответы немного устарели и/или вне темы (т.е. поддержка CORS, которую я сейчас не хочу использовать по разным причинам) и/или, что самое важное, не ссылаются на последние функции из aws-sdk (см.:
https://github.com/aws/aws-sdk-js/issues/13#issuecomment-16085442) или knox (особенно putStream() или его readableStream.pipe(req) вариант,
оба объяснены в документе).
После нескольких часов борьбы я пришел к выводу, что мне нужна помощь (отказ от ответственности: я довольно новичок с потоками).
HTML-форма:
<form action="/uploadPicture" method="post" enctype="multipart/form-data">
<input name="picture" type="file" accept="image/*">
<input type="submit">
</form>
Express bodyParser middleware настраивается следующим образом:
app.use(express.bodyParser({defer: true}))
Обработчик запросов POST:
uploadPicture = (req, res, next) ->
form = new formidable.IncomingForm()
form.parse(req)
form.onPart = (part) ->
if not part.filename
# Let formidable handle all non-file parts (fields)
form.handlePart(part)
else
handlePart(part, form.bytesExpected)
handlePart = (part, fileSize) ->
# aws-sdk version
params =
Bucket: "mybucket"
Key: part.filename
ContentLength: fileSize
Body: part # passing stream object as body parameter
awsS3client.putObject(params, (err, data) ->
if err
console.log err
else
console.log data
)
Однако я получаю следующую ошибку:
{[RequestTimeout: ваше сокетное соединение с сервером не было прочитано или записано в течение периода ожидания. Недействительные соединения будут закрыты.]
message: "Ваше сокетное соединение с сервером не было прочитано или записано в течение периода ожидания. Недействующие соединения будут закрыты. ', код:" RequestTimeout", name: 'RequestTimeout', statusCode: 400, retryable: false}
Версия knox для функции handlePart(), адаптированная таким образом, также ужасно терпит неудачу:
handlePart = (part, fileSize) ->
headers =
"Content-Length": fileSize
"Content-Type": part.mime
knoxS3client.putStream(part, part.filename, headers, (err, res) ->
if err
console.log err
else
console.log res
)
Я также получаю большой объект res с 400 statusCode где-то.
В обоих случаях регион настроен на eu-west-1.
Дополнительные примечания:
node 0,10.12
последний грозный от npm (1.0.14)
последний aws-sdk из npm (1.3.1)
последний нокс от npm (0.8.3)
Ответы
Ответ 1
Ну, в соответствии с создателем Formidable, прямой поток на Amazon S3 невозможно:
В S3 API вам необходимо указать размер новых файлов при их создании. Эта информация недоступна для файлов multipart/form-data, пока они не будут полностью получены. Это означает, что потоковая передача невозможна.
Действительно, form.bytesExpected относится к размеру всей формы, а не к размеру одного файла.
Следовательно, данные должны либо попадать в память или диск на сервере, прежде чем загружаться на S3.
Ответ 2
Использование AWS S3 multipartUpload (s3-upload-stream в качестве рабочего модуля) и node -formidable читаемый поток, вы можете передать поток для загрузки, например this:
var formidable = require('formidable');
var http = require('http');
var util = require('util');
var AWS = require('aws-sdk');
var config = require('./config');
var s3 = new AWS.S3({
accessKeyId: config.get('S3_ACCESS_KEY'),
secretAccessKey: config.get('S3_SECRET_KEY'),
apiVersion: '2006-03-01'
});
var s3Stream = require('s3-upload-stream')(s3);
var bucket = 'bucket-name';
var key = 'abcdefgh';
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
var form = new formidable.IncomingForm();
form.on('progress', function(bytesReceived, bytesExpected) {
//console.log('onprogress', parseInt( 100 * bytesReceived / bytesExpected ), '%');
});
form.on('error', function(err) {
console.log('err',err);
});
// This 'end' is for the client to finish uploading
// upload.on('uploaded') is when the uploading is
// done on AWS S3
form.on('end', function() {
console.log('ended!!!!', arguments);
});
form.on('aborted', function() {
console.log('aborted', arguments);
});
form.onPart = function(part) {
console.log('part',part);
// part looks like this
// {
// readable: true,
// headers:
// {
// 'content-disposition': 'form-data; name="upload"; filename="00video38.mp4"',
// 'content-type': 'video/mp4'
// },
// name: 'upload',
// filename: '00video38.mp4',
// mime: 'video/mp4',
// transferEncoding: 'binary',
// transferBuffer: ''
// }
var start = new Date().getTime();
var upload = s3Stream.upload({
"Bucket": bucket,
"Key": part.filename
});
// Optional configuration
//upload.maxPartSize(20971520); // 20 MB
upload.concurrentParts(5);
// Handle errors.
upload.on('error', function (error) {
console.log('errr',error);
});
upload.on('part', function (details) {
console.log('part',details);
});
upload.on('uploaded', function (details) {
var end = new Date().getTime();
console.log('it took',end-start);
console.log('uploaded',details);
});
// Maybe you could add compress like
// part.pipe(compress).pipe(upload)
part.pipe(upload);
};
form.parse(req, function(err, fields, files) {
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:\n\n');
res.end(util.inspect({fields: fields, files: files}));
});
return;
}
// show a file upload form
res.writeHead(200, {'content-type': 'text/html'});
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload" multiple="multiple"><br>'+
'<input type="submit" value="Upload">'+
'</form>'
);
}).listen(8080);
Ответ 3
Поскольку этот пост настолько старый, и я считаю, что потоковая передача напрямую поддерживается, я потратил много времени на изучение устаревших ответов на эту тему...
Если это помогает любому, кому я мог напрямую передавать с клиента на s3 без необходимости установки пакетов:
https://gist.github.com/mattlockyer/532291b6194f6d9ca40cb82564db9d2a
Сервер предполагает, что req
является объектом потока, в моем случае в xhr (send) использовался объект File, который отправляет двоичные данные в современных браузерах.
const fileUploadStream = (req, res) => {
//get "body" args from header
const { id, fn } = JSON.parse(req.get('body'));
const Key = id + '/' + fn; //upload to s3 folder "id" with filename === fn
const params = {
Key,
Bucket: bucketName, //set somewhere
Body: req, //req is a stream
};
s3.upload(params, (err, data) => {
if (err) {
res.send('Error Uploading Data: ' + JSON.stringify(err) + '\n' + JSON.stringify(err.stack));
} else {
res.send(Key);
}
});
};
Да, это нарушает соглашение, но если вы посмотрите на суть, это намного чище, чем все, что я нашел, полагаясь на другие пакеты.
+1 для прагматизма и благодаря @SalehenRahman за помощь.
Ответ 4
Попробуйте добавить "ContentType" к параметрам "Загрузить" (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property)
...
const params = {
Key,
Bucket: bucketName,
Body: req,
ContentType: 'image/jpg'
};
s3.upload(params, (err, data) => {
if (err) return err;
console.log(data);
});
...
Ответ 5
В промежуточном программном обеспечении Express я использую formidable
PassThrough
вместе с PassThrough
для потоковой передачи файла на S3 (в моем случае на Minio, который совместим с S3 через Minio SDK; и я считаю, что он работает и для AWS S3 с тем же Minio SDK)
Вот пример кода.
const formidable = require('formidable')
const { PassThrough } = require('stream')
const form = new formidable.IncomingForm()
const pass = new PassThrough()
const fileMeta = {}
form.onPart = part => {
if (!part.filename) {
form.handlePart(part)
return
}
fileMeta.name = part.filename
fileMeta.type = part.mime
part.on('data', function (buffer) {
pass.write(buffer)
})
part.on('end', function () {
pass.end()
})
}
form.parse(req, err => {
if (err) {
req.minio = { error: err }
next()
} else {
handlePostStream(req, next, fileMeta, pass)
}
})
И handlePostStream
выглядит так, как handlePostStream
ниже:
const uuidv1 = require('uuid/v1')
const handlePostStream = async (req, next, fileMeta, fileStream) => {
let filename = uuidv1()
try {
const metaData = {
'content-type': fileMeta.type,
'file-name': Buffer.from(fileMeta.name).toString('base64')
}
const minioClient = /* Get Minio Client*/
await minioClient.putObject(MINIO_BUCKET, filename, fileStream, metaData)
req.minio = { post: { filename: '${filename}' } }
} catch (error) {
req.minio = { error }
}
next()
}
Вы можете найти исходный код на GitHub, а также его модульные тесты.