Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює модуль File System (fs) у Node.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Модуль `fs` (File System)** - це вбудований API Node.js для читання, запису та керування файлами на диску. Асинхронні операції виконуються в пулі потоків libuv, не займаючи event loop. ```js const fs = require('fs/promises'); const data = await fs.readFile('file.txt', 'utf8'); // рядок, а не Buffer ``` **Ключове:** завжди передавай `'utf8'` у `readFile`, інакше отримаєш `Buffer`, а не рядок.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Модуль `fs` (File System)** - це вбудований API Node.js для читання, запису, зміни та відстеження файлів і каталогів на диску. Підтримує три стилі роботи: синхронний, на основі callback та Promise. ## Теорія ### TL;DR - Уяви `fs` як ресторан: ти робиш замовлення (файлова операція), кухня (потоки libuv) готує, а стійка реєстрації (event loop) лишається вільною для інших гостей - Три стилі API: callback (застарілий), sync (блокуючий), `fs/promises` (поточний стандарт) - Асинхронні операції `fs` неблокуючі, бо Node делегує дисковий I/O в пул потоків libuv, не займаючи головний потік - `fs/promises` для серверного коду, `readFileSync` тільки в скриптах, потоки для файлів більше ~50MB - Розмір пулу потоків libuv за замовчуванням - 4 (змінюється через `UV_THREADPOOL_SIZE`) ### Швидкий приклад ```js const fs = require('fs/promises'); async function demo() { await fs.writeFile('test.txt', 'Hello Node'); // неблокуючий запис const data = await fs.readFile('test.txt', 'utf8'); // читання як рядок console.log(data); // Hello Node await fs.unlink('test.txt'); // прибираємо за собою } demo().catch(console.error); // event loop обробляє інші запити, поки libuv працює з диском ``` Два моменти варті уваги. Параметр `'utf8'` важливий: без нього отримаєш `Buffer`, а не рядок. І весь ланцюг неблокуючий. ### Як це працює всередині Модуль `fs` звертається до системних викликів ОС (`read(2)` на Linux, `ReadFile` на Windows) через libuv. Коли викликаєш `fs.readFile`, Node не читає файл сам. Він передає завдання в пул потоків libuv (4 потоки за замовчуванням) і одразу повертає контроль event loop. Коли libuv завершує роботу, він додає подію завершення в чергу. V8 підхоплює її і запускає твій callback або резолвить Promise. Синхронні версії пропускають пул потоків і звертаються до ОС напряму з головного потоку. Саме тому `readFileSync` блокує все інше до завершення. Збільшити розмір пулу можна так: `UV_THREADPOOL_SIZE=16 node app.js`. Це питання регулярно зустрічається на технічних інтерв'ю. ### Три стилі API **На основі callback** (оригінальний стиль, досі зустрічається в legacy-коді): ```js const fs = require('fs'); fs.readFile('data.txt', 'utf8', (err, data) => { if (err) throw err; console.log(data); }); ``` **Синхронний** (блокує event loop; підходить тільки для скриптів): ```js const data = fs.readFileSync('data.txt', 'utf8'); console.log(data); ``` **На основі Promise** (поточний стандарт; використовуй `fs/promises`): ```js const fs = require('fs/promises'); const data = await fs.readFile('data.txt', 'utf8'); ``` Різниця у продуктивності між callback і Promise на практиці незначна. Promise додають мікрозавдання, але обробка помилок і ланцюжки набагато чистіші. Я переписав один сервіс логування з вкладених callback на `fs/promises` - код скоротився з 80 рядків до 25 з `async/await`. ### Коли який стиль обирати - CLI-скрипти та завантаження конфігурації при запуску: `readFileSync` нормально. Конкурентності немає, блокування допустиме. - Сервери Express або Fastify: `fs/promises` з `async/await`. Неблокуючий код тримає пропускну здатність під навантаженням. - Файли більше ~50MB: `createReadStream`. Завантаження 10GB файлу через `readFile` призведе до падіння процесу. - Відстеження змін файлів: `fs.watch` для подій на рівні ОС (швидко, мало ресурсів), `fs.watchFile` для polling (надійніше на мережевих файлових системах). - Legacy-кодова база: callback залишається нормальним. Рефакторити немає сенсу, якщо нема проблеми з вкладеністю. ### Основні операції коротко **Читання та запис:** ```js const fs = require('fs/promises'); const text = await fs.readFile('file.txt', 'utf8'); // читання як рядок await fs.writeFile('output.txt', 'Hello'); // створити або перезаписати await fs.appendFile('server.log', `${new Date()} - запит\n`); // додати без перезапису ``` **Каталоги:** ```js await fs.mkdir('logs/prod', { recursive: true }); // вкладені каталоги атомарно (Node 10.12+) const entries = await fs.readdir('src', { withFileTypes: true }); entries.forEach(e => console.log(e.name, e.isDirectory() ? 'каталог' : 'файл')); ``` **Статистика, видалення, перейменування:** ```js const stats = await fs.stat('file.txt'); console.log(stats.size, stats.mtime); // розмір у байтах, час зміни await fs.unlink('old.txt'); // видалити файл await fs.rm('old-folder', { recursive: true }); // видалити каталог await fs.rename('old.txt', 'new.txt'); // перейменувати або перемістити await fs.copyFile('source.txt', 'dest.txt'); // скопіювати ``` ### Типові помилки **Синхронні методи у веб-сервері:** ```js // Неправильно - блокує ВСІ одночасні запити app.get('/', (req, res) => { const data = fs.readFileSync('large.json'); res.send(data); }); // Правильно app.get('/', async (req, res) => { const data = await fs.readFile('large.json', 'utf8'); res.send(data); }); ``` Один заблокований виклик `readFileSync` може опустити сервер з 1000 RPS до 10 RPS під навантаженням. Event loop ставить в чергу всі інші запити за ним. **Забутий параметр кодування в `readFile`:** ```js const data = await fs.readFile('file.txt'); console.log(data); // <Buffer 48 65 6c 6c 6f> - не те, що очікувалось // Виправлення: завжди передавай кодування для текстових файлів const data = await fs.readFile('file.txt', 'utf8'); // повертає рядок ``` За замовчуванням повертається `Buffer`. Це одне з найпопулярніших питань про `fs` на Stack Overflow. **Promise без await у циклі:** ```js // Неправильно - forEach не чекає; записи виконуються в непередбаченому порядку files.forEach(async (file) => { await fs.writeFile(file, 'data'); }); // Правильно for (const file of files) { await fs.writeFile(file, 'data'); } ``` **Піраміда callback (типовий запах legacy-коду):** ```js // Неправильно fs.readFile('a.txt', (err, data) => { fs.writeFile('b.txt', data, (err) => { fs.unlink('a.txt', (err) => { /* і далі глибше */ }); }); }); // Виправлення: перейди на fs/promises const data = await fs.readFile('a.txt', 'utf8'); await fs.writeFile('b.txt', data); await fs.unlink('a.txt'); ``` **Ігнорування race condition при `mkdir`:** ```js // Без { recursive: true } два конкурентних процеси перевіряють "каталог існує?" // і обидва намагаються створити його → другий кидає EEXIST і падає await fs.mkdir('logs', { recursive: true }); // атомарно з Node 10.12, безпечно ``` ### Де зустрічається в реальних проектах - **Express/Fastify:** `fs.appendFile` у request middleware для логування (так само, як Morgan всередині) - **Next.js:** `fs.readdirSync` при білді в `getStaticPaths` для формування списку сторінок із файлової системи - **Webpack:** `fs.readFileSync` для читання маніфестів ресурсів і додавання хешів у назви файлів - **PM2:** `fs.writeFileSync` для запису PID-файлів при управлінні процесами в кластері - **NestJS:** `fs.readFile` для завантаження `.env` або конфігурації перед запуском застосунку Для відстеження файлів у продакшені більшість команд використовує `chokidar` - обгортку над `fs.watch`, яка усуває крайові випадки на macOS і мережевих дисках. ### Follow-up питання **Q:** Який розмір пулу потоків libuv за замовчуванням і як його змінити? **A:** 4 потоки. Задай `UV_THREADPOOL_SIZE=16 node app.js` (максимум 1024) для I/O-інтенсивних застосунків. Актуально для серверів завантаження файлів або пакетної обробки. **Q:** В чому різниця між `fs.watch` та `fs.watchFile`? **A:** `fs.watch` використовує події на рівні ОС (inotify на Linux, FSEvents на macOS): швидко і без зайвого навантаження. `fs.watchFile` працює через polling з інтервалами: повільніше, але надійніше на мережевих файлових системах. **Q:** Як передати 10GB файл через відповідь Express без падіння процесу? **A:** `fs.createReadStream` через pipe до `res`. Чанки йдуть клієнту по мірі читання, споживання пам'яті залишається стабільним. `readFile` завантажить весь файл одразу і процес впаде. **Q:** Як у PM2-кластері писати в спільний лог-файл без пошкодження даних? **A:** `fs.appendFile` є атомарним для невеликих записів на більшості ОС. Для критичних великих записів використовуй `proper-lockfile` або виділений процес-логер. Конкурентний `writeFile` в один файл призводить до перезапису даних. **Q:** Чому `fs.stat` іноді повертає дані симлінку замість цільового файлу? **A:** Не повертає. `fs.stat` завжди слідує за симлінком і повертає дані цілі. Це `fs.lstat` повертає метадані самого симлінку. Часто плутають у тестах, що перевіряють розмір файлів на симлінках. ## Приклади ### Неблокуюче логування запитів в Express-сервері Цей патерн показує, як `fs.appendFile` тримає сервер відповідним під час запису логів на кожному запиті: ```js const express = require('express'); const fs = require('fs/promises'); const app = express(); app.post('/upload', async (req, res) => { const logLine = `${new Date().toISOString()} - User uploaded file\n`; await fs.appendFile('server.log', logLine, 'utf8'); // запис іде в потік libuv, event loop вільний для інших запитів res.send('Logged'); }); app.listen(3000); // server.log: "2026-04-14T22:00:00.000Z - User uploaded file" ``` Event loop не блокується. Інші вхідні запити обробляються паралельно, поки libuv пише на диск. ### ENOENT та конкурентне створення каталогів Типова пастка у налаштуваннях з кількома процесами, наприклад у PM2. Два воркери намагаються створити один каталог при запуску: ```js const fs = require('fs/promises'); const path = require('path'); async function safeMkdirWrite(dir, filename) { try { await fs.mkdir(dir, { recursive: true }); // атомарно з Node 10.12 await fs.writeFile(path.join(dir, filename), 'data'); console.log('Записано'); } catch (err) { if (err.code === 'EEXIST') { console.log('Каталог вже існував, продовжуємо'); } else { throw err; // непередбачена помилка, прокидаємо далі } } } safeMkdirWrite('logs/prod', 'app.log'); // Виведе: Записано // Без { recursive: true }: конкурентні виклики кидають ENOTDIR ``` Опція `{ recursive: true }` робить операцію безпечною. Без неї другий конкурентний виклик кидає `EEXIST` і воркер падає. ### Потокова передача великого файлу для економії пам'яті ```js const fs = require('fs'); function streamFile(filePath, destination) { const readStream = fs.createReadStream(filePath, { encoding: 'utf8' }); const writeStream = fs.createWriteStream(destination); readStream.pipe(writeStream); readStream.on('error', (err) => console.error('Помилка читання:', err.message)); writeStream.on('finish', () => console.log('Передачу завершено')); } streamFile('large-data.csv', 'output.csv'); // Споживання пам'яті стабільне незалежно від розміру файлу // fs.readFile для 10GB файлу = падіння процесу через перевищення пам'яті ``` Ключова різниця: `readFile` виділяє пам'ять для всього файлу одразу. `createReadStream` тримає в пам'яті лише поточний чанк і відкидає його після відправки.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.