Загрузка файла AWS SDK в S3 через Node/Express с использованием потока PassThrough — файл всегда поврежден

Это довольно просто. Используя этот код, любой загруженный файл изображения будет поврежден и не может быть открыт. PDF-файлы выглядят нормально, но я заметил, что он вводит значения в текстовые файлы. Это правильный размер файла в s3, а не нулевой, как будто что-то пошло не так. Я не уверен, что это проблема с Express, SDK или их комбинацией? Это Почтальон? Я построил нечто подобное в рабочем проекте в марте этого года, и оно работало безупречно. У меня больше нет доступа к этому коду для сравнения.

Никаких ошибок, никаких признаков каких-либо проблем.

const aws = require("aws-sdk");
const stream = require("stream");
const express = require("express");
const router = express.Router();

const AWS_ACCESS_KEY_ID = "XXXXXXXXXXXXXXXXXXXX";
const AWS_SECRET_ACCESS_KEY = "superSecretAccessKey";
const BUCKET_NAME = "my-bucket";
const BUCKET_REGION = "us-east-1";

const s3 = new aws.S3({
    region: BUCKET_REGION,
    accessKeyId: AWS_ACCESS_KEY_ID,
    secretAccessKey: AWS_SECRET_ACCESS_KEY
});

const uploadStream = key => {
    let streamPass = new stream.PassThrough();
    let params = {
        Bucket: BUCKET_NAME,
        Key: key,
        Body: streamPass
    };
    let streamPromise = s3.upload(params, (err, data) => {
        if (err) {
            console.error("ERROR: uploadStream:", err);
        } else {
            console.info("INFO: uploadStream:", data);
        }
    }).promise();
    return {
        streamPass: streamPass,
        streamPromise: streamPromise
    };
};

router.post("/upload", async (req, res) => {
    try {
        let key = req.query.file_name;
        let { streamPass, streamPromise } = uploadStream(key);
        req.pipe(streamPass);
        await streamPromise;
        res.status(200).send({ result: "Success!" });
    } catch (e) {
        res.status(500).send({ result: "Fail!" });
    }
});

module.exports = router;

Вот мой package.json:

{
  "name": "expresss3streampass",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "aws-sdk": "^2.812.0",
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "express": "~4.16.1",
    "morgan": "~1.9.1"
  }
}

ОБНОВЛЯТЬ:

После дальнейшего тестирования я заметил, что Postman изменяет текстовые файлы. Например, этот исходный файл:

{
    "question_id": null,
    "position_type_id": 1,
    "question_category_id": 1,
    "position_level_id": 1,
    "question": "Do you test your code before calling it \"done\"?",
    "answer": "Candidate should respond that they at least happy path test every feature and bug fix they write.",
    "active": 1
}

... выглядит так после попадания в ведро:

----------------------------472518836063077482836177
Content-Disposition: form-data; name = "file"; filename = "question.json"
Content-Type: application/json

{
    "question_id": null,
    "position_type_id": 1,
    "question_category_id": 1,
    "position_level_id": 1,
    "question": "Do you test your code before calling it \"done\"?",
    "answer": "Candidate should respond that they at least happy path test every feature and bug fix they write.",
    "active": 1
}
----------------------------472518836063077482836177--

Я должен думать, что это проблема. Почтальон — единственное, что изменилось в этом уравнении с тех пор, как этот код впервые заработал у меня. Заголовки моих запросов выглядят так:

Я был тем, кто изначально добавил заголовок «application/x-www-form-urlencoded». Если я использую это сейчас, я получаю файл с 0 байтами в ведре.

Есть ли конкретная причина, по которой вы не хотите использовать multer?

relief.melone 21.12.2020 23:02

@relief.melone Да, это потоковая передача файлов, а не вынос контейнера с файлом, который может быть слишком большим, или блокировка потока при большой загрузке.

Tsar Bomba 22.12.2020 14:04

Вы также можете использовать потоки с multer вместо того, чтобы сначала загружать его в контейнер. Я не уверен, но я был бы уверен, что multer-s3-storage делает именно это, и я делаю то же самое в механизме хранения, который я написал для multer gitlab.com/relief-melone/multer-s3-sharp- изменение размера. Однако я до сих пор не уверен, что вызывает проблемы с вашим кодом, поскольку мой подход почти такой же, как у вас (и я также использую Postman для тестирования). Я все равно посмотрю на это поближе, как только у меня будет время. Я понимаю, что вы не только хотите, чтобы все заработало, но и понимаете, что идет не так.

relief.melone 22.12.2020 14:40

Цель вашего сервера - только забрать файл у пользователя и поместить его на S3?

Benjamin Filiatrault 25.12.2020 17:35
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
Раскрытие чувствительных данных
Раскрытие чувствительных данных
Все внешние компоненты, рассмотренные здесь до сих пор, взаимодействуют с клиентской стороной. Однако, если они подвергаются атаке, они не...
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Мы провели Twitter Space, обсудив несколько проблем, связанных с последними дополнениями в Angular. Также прошла Angular Tiny Conf с 25 докладами.
Руководство ChatGPT по продаже мини JS-файлов
Руководство ChatGPT по продаже мини JS-файлов
JS-файл - это файл, содержащий код JavaScript. JavaScript - это язык программирования, который в основном используется для добавления интерактивности...
6
4
4 491
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Насколько я могу судить, Postman ведет себя так, как должен — «вставка текста» на самом деле является веб-стандартом, используемым для идентификации/разграничения файлов при загрузке. Пожалуйста, ознакомьтесь с этим веб-документом MDN , а также этим, чтобы понять почему.

На самом деле эта часть внедряется независимо от типа файла:

let streamPass = new stream.PassThrough();
// adding this
const chunks = [];
streamPass.on('data', (chunk) => chunks.push(chunk) );
streamPass.on("end", () => {
    body = Buffer.concat(chunks).toString();
    console.info(chunks, chunks.length)
    console.info("finished", body);  // <-- see it here
});

Я попробовал несколько методов, чтобы контролировать/изменить это, но не повезло с простым методом - со стороны Postman, я не думаю, что это параметр, который можно изменить, и со стороны NodeJS... Я имею в виду, что это возможно, но решение, скорее всего, будет неуклюжим/сложным, что, как я подозреваю, вам не нужно. (хотя могу ошибаться...)

Учитывая вышеизложенное, я присоединяюсь к @relief.melone и рекомендую multer как простое решение.

Если вы хотите использовать multer с streams, попробуйте следующее: (я указал, где я внес изменения в ваш код):

// const uploadStream = (key) => {
const uploadStream = (key, mime_type) => {      // <- adding the mimetype

    let streamPass = new stream.PassThrough();
    
    let params = {
        Bucket: BUCKET_NAME,
        Key: key,
        Body: streamPass,
        ACL: 'public-read', // <- you can remove this
        ContentType: mime_type  // <- adding the mimetype
    };
    let streamPromise = s3.upload(params, (err, data) => {
        if (err) {
            console.error("ERROR: uploadStream:", err);
        } else {
            console.info("INFO: uploadStream:", data);
        }
    }).promise();
    
    return {
        streamPass: streamPass,
        streamPromise: streamPromise
    };
};

// router.post("/upload", async (req, res) => {
router.post("/upload", multer().single('file'), async (req, res) => {      // <- we're adding multer
    try {
        
        let key = req.query.file_name;
        // === change starts here 

            // console.info(req.file); // <- if you want to see, uncomment this file

            let { streamPass, streamPromise } = uploadStream(key, req.file.mimetype);   // adding the mimetype

            var bufferStream = new stream.PassThrough();

            bufferStream.end(req.file.buffer);

            bufferStream.pipe(streamPass); // no longer req.pipe(streamPass);

        // === change ends here 
        await streamPromise;
        
        res.status(200).send({ result: "Success!" });
    } catch (e) {
        console.info(e)
        res.status(500).send({ result: "Fail!" });
    }
});

Отличный ответ, спасибо! Это отлично сработало для меня, однако я все еще немного обеспокоен тем, что это не полностью потоковая передача. Как отметил Ричард Данн выше, не потребуется ли специальный обработчик для фактической потоковой передачи данных?

Tsar Bomba 23.12.2020 00:22

Рад, что сработало 🎉. Я на пороге пользовательского обработчика (кстати, я также нашел эту статью, где кто-то использовал аналогичную настройку, но для потоковой передачи в Google Cloud: medium.com/better-programming/…) Но @Richard Dunn может быть прав. Поскольку Multer теперь выглядит хорошо для вас, я бы порекомендовал прочитать репозиторий Multer (он хорошо объяснен) и продолжить тестирование, чтобы увидеть, что работает для вас.

Deolu A 23.12.2020 11:33
Ответ принят как подходящий

Мультер - это то, что нужно.

Он предоставляет несколько различных режимов, но, насколько я могу судить, вам нужно написать собственный обработчик хранилища, чтобы получить доступ к базовому потоку, иначе он будет буферизовать все данные в памяти и выполнять обратный вызов только после того, как это будет сделано. .

Если вы отметите req.file в своем обработчике маршрута, Multer обычно предоставляет буфер под полем buffer, но его больше нет, так как я ничего не передаю в обратном вызове, поэтому я достаточно уверен, что поток идет так, как ожидалось.

Ниже приведено рабочее решение.

Примечание: parse.single('image') передается в обработчик маршрута. Это относится к имени поля из нескольких частей, которое я использовал.

const aws = require('aws-sdk');
const stream = require('stream');
const express = require('express');
const router = express.Router();
const multer = require('multer')

const AWS_ACCESS_KEY_ID = "XXXXXXXXXXXXXXXXXXXX";
const AWS_SECRET_ACCESS_KEY = "superSecretAccessKey";
const BUCKET_NAME = "my-bucket";
const BUCKET_REGION = "us-east-1";

const s3 = new aws.S3({
    region: BUCKET_REGION,
    accessKeyId: AWS_ACCESS_KEY_ID,
    secretAccessKey: AWS_SECRET_ACCESS_KEY
});

const uploadStream = key => {
    let streamPass = new stream.PassThrough();
    let params = {
        Bucket: BUCKET_NAME,
        Key: key,
        Body: streamPass
    };
    let streamPromise = s3.upload(params, (err, data) => {
        if (err) {
            console.error('ERROR: uploadStream:', err);
        } else {
            console.info('INFO: uploadStream:', data);
        }
    }).promise();
    return {
        streamPass: streamPass,
        streamPromise: streamPromise
    };
};

class CustomStorage {
    _handleFile(req, file, cb) {
        let key = req.query.file_name;
        let { streamPass, streamPromise } = uploadStream(key);
        file.stream.pipe(streamPass)
        streamPromise.then(() => cb(null, {}))
    }
}

const storage = new CustomStorage();
const parse = multer({storage});

router.post('/upload', parse.single('image'), async (req, res) => {
    try {
        res.status(200).send({ result: 'Success!' });
    } catch (e) {
        console.info(e)
        res.status(500).send({ result: 'Fail!' });
    }
});

module.exports = router;

Обновление: лучшее решение

Решение на основе Multer , которое я представил выше, немного хакерское. Поэтому я заглянул под капот, чтобы увидеть как это работает . Это решение просто использует Busboy для анализа и потоковой передачи файла. Multer на самом деле является просто оболочкой для этого с некоторыми удобными функциями дискового ввода-вывода.

const aws = require('aws-sdk');
const express = require('express');
const Busboy = require('busboy');
const router = express.Router();

const AWS_ACCESS_KEY_ID = "XXXXXXXXXXXXXXXXXXXX";
const AWS_SECRET_ACCESS_KEY = "superSecretAccessKey";
const BUCKET_NAME = "my-bucket";
const BUCKET_REGION = "us-east-1";

const s3 = new aws.S3({
    region: BUCKET_REGION,
    accessKeyId: AWS_ACCESS_KEY_ID,
    secretAccessKey: AWS_SECRET_ACCESS_KEY
});

function multipart(request){
    return new Promise(async (resolve, reject) => {
        const headers = request.headers;
        const busboy = new Busboy({ headers });
        // you may need to add cleanup logic using 'busboy.on' events
        busboy.on('error', err => reject(err))
        busboy.on('file', function (fieldName, fileStream, fileName, encoding, mimeType) {
            const params = {
                Bucket: BUCKET_NAME,
                Key: fileName,
                Body: fileStream
            };
            s3.upload(params).promise().then(() => resolve());
        })
        request.pipe(busboy)
    })
}

router.post('/upload', async (req, res) => {
    try {
        await multipart(req)
        res.status(200).send({ result: 'Success!' });
    } catch (e) {
        console.info(e)
        res.status(500).send({ result: 'Fail!' });
    }
});

module.exports = router;

Лучшее решение сейчас не работает с AWS, оно возвращает ошибку: «Предоставленный вами заголовок подразумевает функциональность, которая не реализована». Может какие-то данные заголовка пропущены при разрешении Busboy??

QUANG Tuấn Vũ 10.10.2021 06:28

Другие вопросы по теме