Это результат нескольких дней проб и ошибок. Я понятия не имел о потоках и прочем, поэтому это заняло так много времени 😀
Вам понадобятся следующие библиотеки:
- multer
- @fast-csv/parse
- streamifier
Создайте форму для загрузки CSV файлов — multipart/form-data
.
Загрузка файлов должна осуществляться через multipart/form-data
. Это то, с чем я познакомился недавно, и, возможно, это будет предметом другого поста. Пока что я пропущу это.
multer
multer
возьмет файл и поместит его в req.file
. Не надейтесь найти файл в req.body
— он будет содержать только текстовые данные полей формы. 90% учебников по multer объясняют, как сохранить входящий файл в директории. Меня это не волнует, потому что файл будет храниться на сервере, где у меня нет прав на запись, поэтому я хочу, чтобы файл хранился в памяти.
const multer = require("multer");
const parseCsv = multer().single("whatever-name-you-gave-to-the-input-field-in-your-form");
module.exports = {parseCsv}
Это промежуточное ПО, которое поместит файл в req.file
.
fast-csv и streamifier
req.file
будет иметь свойство buffer
, но оно не читаемо для createReadStream
node. Если вы попробуете fs.createReadStream(buffer)
, то, скорее всего, получите ошибку, говорящую, что это не файл, или что-то подобное. Хотя Node createReadStream
принимает экземпляр Buffer
(а наш buffer
это именно экземпляр), этот экземпляр не читается createReadStream
. Я узнал об этом в этом ответе SO. Решение, которое я нашел? streamifier
, о котором я впервые узнал здесь. Если вы посмотрите на его исходный код, он делает некоторую магию для преобразования буфера в req.file
в читаемый буфер, который передается в createReadStream
. Я был рад, что нашел эту библиотеку.
Итак, вы создаете поток следующим образом
const { buffer } = req.file;
streamifier.createReadStream(buffer)
@fast-csv/parse
@fast-csv/parse
берет поток с данными из csv и вызывает пару событий для разбора содержимого файла. Он вызывает .on('data', data => callback)
для каждой строки, так что вы можете делать с ней все, что захотите. Когда все строки будут разобраны, вызывается .on('end', rowCount => callback)
. Есть событие .on('error', callback)
, которое, как я полагаю, связано с возможностями валидации, но я его еще не пробовал.
Вы можете импортировать fast-csv как csv
и затем вызвать .pipe(csv.parse())
(см. пример ниже). Также вы можете передать опции в csv.parse()
, те, которые я использовал до сих пор: headers: true
(пропускает строку заголовка из файла csv, см. документацию здесь) и ignoreEmpty: true
(игнорирует пустые строки, см. документацию здесь).
Моя первая итерация заключалась в том, чтобы поместить создание документа при разборе каждой строки. Ошибка из-за асинхронного характера сохранения данных в БД и синхронного характера разбора CSV. Я обнаружил, что событие 'end'
срабатывает до сохранения первого документа, и это испортило мою стратегию и ответы сервера.
Я провел небольшое исследование и нашел стратегию, которая хорошо работает: добавьте разобранный ряд (который возвращается как объект) в массив в памяти, и вы вызываете Model.create([ARRAY_OF_OBJECTS])
на 'end'
событие. Вам нужно сделать это async и определить ответ сервера клиенту. Как и в этом случае, мне кажется, что это работает хорошо:
const csv = require("@fast-csv/parse");
const streamifier = require("streamifier");
// somewhere below
router.post("/endpoint", [multerMiddlewareExplainedAbove], (req, res) => {
const { buffer } = req.file;
const dataFromRows = [];
streamifier
.createReadStream(buffer)
.pipe(csv.parse({ headers: true, ignoreEmpty: true })) // <== this is @fast-csv/parse!!
.on("data", (row) => {
dataFromRows .push(row);
})
.on("end", async (rowCount) => {
try {
const data = await MyModelName.create(dataFromRows );
res.status(200).json({ rowCount, data });
} catch (error) {
res.status(400).json({ error});
}
});
});
Надеюсь, это имеет смысл. Я буду добавлять материал по мере его обнаружения. Спасибо, что прочитали (: