Перейти до основного вмісту

Formidable

Почнемо із перевіреного ветерана. Установка пакета наступна

npm install formidable

Розглянемо найпростіший приклад для завантаження файлу. Мається на увазі, що ми вже встановили всі пакети npm i , які ми використовуємо. Спочатку підключимо ці пакети

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const formidable = require('docs/additional_materials/additional-work-with-files/formidable');
const app = express();

Після цього визначимо дві змінні для зберігання шляхів. У першій змінній uploadDir ми зберігаємо шлях де зберігається файл при початковому завантаженні, другий storeImage шлях де зберігається файл остаточно.

const uploadDir = path.join(process.cwd(), 'uploads');
const storeImage = path.join(process.cwd(), 'images');

Чому ми використовуємо два способи зберігання файлу?

Перша причина, що після завантаження нам знадобиться додаткова обробка файлу. Наприклад, зменшити або навпаки збільшити розмір зображення, зробити його квадратним і т.д. А значить, після обробки ми його перенесемо в нове місце.

Друга причина при завантаженні або обробці файлу може статися не врахована нами помилка, плюс пакети часто зберігають спочатку файл під якимось тимчасовим іменем без розширення і лише після остаточного завантаження ми можемо його змінити. Таким чином на шляху uploadDir у нас тимчасове сховище, і якщо там будуть залишатися файли, отже, у нас відбуваються якісь помилки при завантаженні або обробці та їх необхідно виправити, якщо це можливо.

Плюс підходу ще в тому, що залишкові файли можна буде спокійно видаляти з папки не турбуючись видалити щось потрібне.

Щоб обробити завантаження файлів, нам необхідно створити екземпляр formidable

const form = formidable(options);

І розпарсити дані методом form.parse

form.parse(req, (err, fields, files) => {
// ...
});

Метод викликає callback функцію, в яку передається три параметри, err помилка якщо вона сталася, fields це об'єкт який містить звичайні поля форми та останнім йде параметр files , який може бути масивом, якщо дозволено мультизавантаження файлів, або об'єкт якщо файл завантажується один.

Нам хотілося б позбутися callback функції. Напишемо спеціальну обгортку, яка повертатиме проміс із результатами парсингу форми.

const parseFrom = (form, req) => {
return new Promise((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) {
return reject(err);
}
resolve({ fields, files });
});
});
};

Далі опишемо обробник маршруту /upload для HTTP методу POST

app.post('/upload', async (req, res, next) => {
const form = formidable({ uploadDir, maxFileSize: 2 * 1024 * 1024 });
const { fields, files } = await parseFrom(form, req);
const { path: temporaryName, name } = files.picture;
const { description } = fields;

const fileName = path.join(storeImage, name);
try {
await fs.rename(temporaryName, fileName);
} catch (err) {
await fs.unlink(temporaryName);
return next(err);
}
res.json({ description, message: 'Файл успішно завантажено', status: 200 });
});

У ньому ми створюємо екземпляр formidable

const form = formidable({ uploadDir, maxFileSize: 2 * 1024 * 1024 });

де в опціях вказуємо,

  • uploadDir шлях куди треба зберігати файл
  • maxFileSize максимальний розмір файлів, що завантажуються 2Мб, що необхідно робити завжди - контролювати розмір файлів, що завантажуються

Далі ми парсимо форму за допомогою нашої функції parseFrom і отримуємо текстові поля fields та об'єкт з файлом files.

У нашому прикладі ми припускаємо, що ім'я поля з файлом дорівнює picture. І за допомогою деструктуризації ми отримуємо шлях де знаходиться файл temporaryName і його оригінальне ім'я name

const { path: temporaryName, name } = files.picture;

Потім даємо нове ім'я файлу, у нашому випадку ми залишаємо його тим самим, але хорошою практикою є прив'язувати нове ім'я до дати або рандомного хешу, щоб випадково однакові файли не перезаписали один одного.

const fileName = path.join(storeImage, name);

І переносимо файл у постійне сховище

try {
await fs.rename(temporaryName, fileName);
} catch (err) {
await fs.unlink(temporaryName);
return next(err);
}

Якщо сталася помилка перенесення, не забуваємо видаляти тимчасовий файл.

await fs.unlink(temporaryName);

Якщо все пройшло вдало, в кінці відправляємо відповідь у вигляді JSON

res.json({ description, message: 'Файл успішно завантажено', status: 200 });

Повний код нашої програми наступний

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const formidable = require('docs/additional_materials/additional-work-with-files/formidable');
const app = express();

const uploadDir = path.join(process.cwd(), 'uploads');
const storeImage = path.join(process.cwd(), 'images');

const parseFrom = (form, req) => {
return new Promise((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) {
return reject(err);
}
resolve({ fields, files });
});
});
};

app.post('/upload', async (req, res, next) => {
const form = formidable({ uploadDir, maxFileSize: 2 * 1024 * 1024 });
const { fields, files } = await parseFrom(form, req);
const { path: temporaryName, name } = files.picture;
const { description } = fields;

const fileName = path.join(storeImage, name);
try {
await fs.rename(temporaryName, fileName);
} catch (err) {
await fs.unlink(temporaryName);
return next(err);
}
res.json({ description, message: 'Файл успішно завантажено', status: 200 });
});

const isAccessible = path => {
return fs
.access(path)
.then(() => true)
.catch(() => false);
};

const createFolderIsNotExist = async folder => {
if (!(await isAccessible(folder))) {
await fs.mkdir(folder);
}
};

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.json({ message: err.message, status: err.status });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, async () => {
createFolderIsNotExist(uploadDir);
createFolderIsNotExist(storeImage);
console.log(`Server running. Use on port:${PORT}`);
});

Тут є дві функції, що заслуговують на нашу увагу. Функція isAccessible повертає логічне вираження залежно від того існує чи ні папка, а функція createFolderIsNotExist створює папку, якщо її не існує. І коли стартує наша програма ми перевіряємо чи існують папки uploadDir та storeImage і якщо їх немає створюємо.

app.listen(PORT, async () => {
createFolderIsNotExist(uploadDir);
createFolderIsNotExist(storeImage);
console.log(`Server running. Use on port:${PORT}`);
});

Давайте тепер спробуємо виконати завантаження файлу за допомогою Postman зімітувати роботу форми у режимі multipart/form-data

upload

Необхідно вибрати під час надсилання кодування form-data, як зазначено на малюнку. І головне завантажити файл для надсилання його на сервер. У цьому режимі ключ необхідно перевести з текстового типу у файловий

upload

Якщо все зробити правильно при надсиланні повідомлення, файл буде завантажено на сервер і зберігається в папці images , а від сервера ми отримаємо відповідь як на малюнку.

Повний код прикладу на Github Gist