Мне нужно руководство или объяснение, как использовать дуплексные потоки загрузки в node.js. Или какое-то подтверждение правильности моего подхода.
Общая идея состоит в том, чтобы загрузить файл в приложении angular на PHP сервер через graphql:
[Angular app] --> [GraphQL] --> [PHP Storage - some image manipulation] --> cloud or other storage
В конце концов, я был успешно, но я не могу устоять перед чувством, что это можно было бы сделать лучше. Вот некоторые подробности того, что я обнаружил и чего добился.
В качестве сервера GraphQL я использую Apollo . Общая идея заключается в том, что Apollo предоставляет Upload
скаляр, который предоставляет поток для чтения загруженного файла в преобразователе мутаций.
Согласно документации Мне удалось отправить файл из angular в GQL server и создать там поток чтения, но я не смог отправить его через HTTP во внешнюю службу. Одно замечание: я использую ax ios для HTTP-запроса, но я пробовал также с другими инструментами с тем же результатом.
Подход 1. Отправить поток как есть
Во многих потоках stackoverflow я нашел один общий подход: упаковывать поток непосредственно в объект FormData
и просто передавать его как тело запроса, поэтому мой код выглядел так:
exports.upload = async function(stream, token) {
const { createReadStream, filename, mimetype } = await stream;
const imageIncomingStream = createReadStream();
const formData = new FormData();
formData.append('file', imageIncomingStream, {
contentType: mimetype,
filename: filename
});
const headers = formData.getHeaders({
'Authorization' : token
});
axios.post('MY ENDPOINT HERE', formData, { headers });
};
, но Проблема заключалась в том, что поток был отправлен не полностью, например, ax ios или данные формы не могли прочитать его перед отправкой. PHP Сервер получал загрузку, но файл был поврежден. Поэтому я подумал, что, может быть, мне следует сначала прочитать этот поток, а затем отправить его. Итак, возьмите номер два
Подход 2: Прочтите поток перед отправкой
exports.upload = async function(stream, token) {
const { createReadStream, filename, mimetype } = await stream;
const imageIncomingStream = createReadStream();
const formData = new FormData();
imageIncomingStream.on('end', () => {
formData.append('file', imageIncomingStream, {
contentType: mimetype,
filename: filename
});
const headers = formData.getHeaders({
'Authorization' : token
});
axios.post('MY ENDPOINT HERE', formData, { headers });
});
imageIncomingStream.resume();
};
с этим подходом я мог видеть, что отправляется весь файл, но по какой-то причине запрос не выполнялся . Сервер некоторое время зависал, а затем вылетал с ошибкой Error: socket hang up
. Я попытался добавить длину содержимого, например,
formData.getLength((err, len) => {
const headers = formData.getHeaders({
'content-length' : len,
'Authorization' : token
});
axios.post('MY ENDPOINT HERE', formData, { headers });
});
, но это тоже не помогло. Также пробовал с imageIncomingStream.destroy();
, потому что я подумал, что, возможно, поток записи каким-то образом блокирует его, но снова безуспешно. Одно важное замечание: запрос никогда не выполнялся или, по крайней мере, он никогда не доходил до сервера PHP.
На этом этапе я не был уверен, использую ли я правильные инструменты, или, может быть, что-то не так с конфигурации серверов, поэтому я попытался отправить локальный файл вот так. Вместо использования потока, предоставляемого Apollo, я создал обычный поток чтения для одного из локальных файлов, например const imageIncomingStream = fs.createReadStream(__dirname + '/image.png');
, и он работал как шарм с первым подходом, поэтому нет необходимости вручную читать весь поток. Ax ios или FormData смогли прочитать поток и отправить его на мой сервер. Итак, это привело меня к созданию окончательного и рабочего подхода.
Подход 3: создать поток чтения из файла tmp
Я заметил, что как только входящий поток полностью прочитан, к нему прикреплен путь, указывающий на tmp файл на сервере. Итак, я понял, что могу прочитать этот файл, поскольку работают обычные потоки чтения. Вот как это выглядит сейчас
exports.upload = async function(stream, shopId, productId, token) {
const { createReadStream, filename, mimetype } = await stream;
const imageIncomingStream = createReadStream();
finished(imageIncomingStream, (err) => {
if (err) {
console.error('Stream failed.', err);
} else {
const tmpImageStream = fs.createReadStream(imageIncomingStream.path);
const formData = new FormData();
imageIncomingStream.destroy();
formData.append('file', tmpImageStream, {
contentType: mimetype,
filename: filename
});
const headers = formData.getHeaders({
'Authorization' : token
});
axios.post('MY ENDPOINT HERE', formData, { headers });
}
});
imageIncomingStream.resume();
};
Как я сказал в начале, это работает, но я чувствую, что это можно было бы сделать лучше. Итак, вот мой вопрос: есть ли способ использовать входящий поток (который является дуплексным, если это имеет значение) вместо создания нового?