Тема: Як я файли на nodejs завантажував
Нещодавно почав вивчати NodeJs та дійшов до моменту, коли пояснювалось, як обробляти на сервері HTML форми.
З формами, що містять прості дані, як от текст - все просто, а от з файлами вже складніше.
В книзі відразу почали розповідати про модуль під назвою formidable, https://www.npmjs.com/package/formidable , але не розповіли про те, як воно робе.
І найгірше, що коли я почав гуглити "nodejs handle multipart/form-data", то не знайшов "нативних" прикладів, без додаткових залежностей, всюди використовувався або ж той самий formidable, або ж multer, а мені хотілось зрозуміти, як воно працює на простому прикладі, без читання сирців того formidable, котрий, швидше за все, використовує цілу купу різних інших модулів.
Тому я пішов складним шляхом, виводив відправлені дані в консоль на стороні серверу, та порівнював їх з тим, що показував vim, коли я відкривав відправлені файли в ньому. І методом проб та помилок зумів написати досить кривий, але працюючий міні-сервер, котрий дозволяє завантажувати PNG та JPG файли.
const http = require("http");
const fs = require("fs");
const join = require("path").join;
// масив з іменами картинок
const pics = [];
// дуже простий сервер, котрий обробляє лише GET та POST запити
const server = http.createServer((req, res) => {
if (req.method === "GET") {
get(req, res);
} else if (req.method === "POST") {
add(req, res);
}
});
server.listen(3000);
/**
* Дана функція обробляє POST запити
*/
function add(req, res) {
let body = "";
// Вказуємо, що будемо отримувати бінарні дані
req.setEncoding("binary");
// Дані можуть передаватись частинками, тому тут ми зліплюємо ці частинки в одну купу
req.on(`data`, (chunk) => {
body += chunk;
});
// Ця подія відбувається після того, як ми отримали всі дані, отже змінна body міститиме всі необхідні для обробки дані
req.on(`end`, () => {
// Отримуємо "розділювач"
const boundary = req.headers[`content-type`].split("boundary=")[1];
// Використовуємо "розділювач" для "нарізання" даних на частинки, котрі будемо обробляти
body.split(boundary).forEach((item) => {
// -- сигналізує, що ми обробили всі дані. Також ми ігноруємо дані без Content-Type, як от простий текст.
if (item === "--" || !item.includes(`Content-Type`)) return;
// Отримуємо назву файлу, та заміняємо пробіли на нижнє підкреслення
const name = item
.match(new RegExp(`filename="(.+)"`))[1]
.replaceAll(` `, `_`);
// Отримуємо тип даних
const match = item.match(new RegExp(`Content-Type: (.+)\r\n`));
const contentType = match[1];
let format;
if (contentType.includes(`png`)) {
format = `PNG`;
} else if (contentType.includes(`jpeg`)) {
format = `jpeg`;
}
// Це додаткова перевірка. Код виконуватиметься лише для jpeg та png файлів
if (format) {
// Отримуємо сам файл, тобто дані того файлу, що був збереженим на компі користувача
const file = getImage(item, format);
// Синхронно зберігаємо файл в поточній директорії
fs.writeFileSync(`./${name}`, file, { encoding: "binary" });
// Заносимо ім'я файлу в масив
pics.push(name);
}
});
// Повертаємо користувачу дані, що будуть відображені в бравзері, чи ще десь там.
get(req, res);
});
}
// Дана функція "вирізає" корисні дані картинки
function getImage(data, format) {
const indexStart = data.indexOf(format);
// Методом тику я підібрав необхідні індекси
let indexOffset = format === "jpeg" ? 8 : -1;
return data.substring(indexStart + indexOffset, data.length - 4);
}
// Ця функція відправляє користувачу html файл, що міститиме в собі картинки, котрі збережені в масиві pics
// Для завантаження файлів необхідно вказати, що форма завантажуватиме саме файли, а для цього треба вказати відповідний enctype
function get(req, res) {
if (req.url === "/") {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.img {
max-width: 250px;
margin: 15px;
}
</style>
</head>
<body>
<form action="/" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<button>Додати файл</button>
</form>
${pics.map((pic) => `<img class="img" src="${pic}">`).join("")}
</body>
</html>
`;
res.setHeader(`Content-Type`, `text/html`);
res.setHeader(`Content-Length`, Buffer.byteLength(html));
res.end(html);
/**
* Коли користувач отримує HTML, що містить картинки (тобто, теги img з вказаним src),
* бравзер робить запит використовуючи значення src, як адресу запиту.
* Наприклад: http://localhost:3000/myPic.jpg
* Даний блок перевіряє, чи в запиті міститься "png", або "jpg", і якщо так, тоді сервер намагається завантажити
* відповідні файли з поточної директорії (у цьому випадку він спробує завантажити файл myPic.jpg)
*/
} else if (req.url.includes("png") || req.url.includes("jpg")) {
const readStream = fs.createReadStream(join(__dirname, req.url));
// Це крута фіча, про котру можна дізнатись більше, якщо загуглити nodejs Streams
readStream.pipe(res);
} else {
// В решті випадків (коли бравзер запитує якісь інші файли) просто повертаємо ok
res.end("ok");
}
}