1

Тема: Як я файли на 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");
  }
}