Файлове
Проблем
Програмата забравя
Пишеш програма за разходите си. Въвеждаш ги вечер, затваряш я, а на другия ден тя не знае нищо за вчера. Ето най-краткият пример:
let visits = 0;visits++;console.log(`Пусната ${visits} път.`);Пусната 1 път.Колкото пъти и да я пуснеш, казва „1 път“. Променливите живеят в паметта, докато програмата работи. Когато свърши, паметта се освобождава и всичко изчезва.
За да помни, програмата трябва да запише данните там, където остават и след нея: във файл на диска.
Идеята
Файл с node:fs
Ето цяла програма, която записва бележка във файл и я чете обратно:
import { readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("notes.txt", "Купи мляко\nПлати тока до 15-ти\n"); const text = readFileSync("notes.txt", "utf8"); process.stdout.write(text);} catch (e) { console.log(`Проблем с файла: ${e.message}`);}Купи млякоПлати тока до 15-тиimport { readFileSync, writeFileSync } from "node:fs";взима две функции от модулаnode:fs(file system, „файловата система“). Той идва с Node и не се инсталира.writeFileSync(име, текст)записва текста и създава файла, ако го няма."notes.txt"е файл с това име в папката, от която е пусната програмата.readFileSync(име, "utf8")чете целия файл като текст."utf8"е кодировката: как байтовете стават букви.- „Sync“ значи, че програмата чака, докато файлът бъде записан или прочетен, и чак тогава продължава.
- Ако нещо не стане (дискът е пълен, файлът е заключен или го няма), функцията хвърля грешка. JavaScript не те задължава да я хванеш, но без
catchпрограмата спира. process.stdout.write(text)печата текста както е.console.logби добавил още един нов ред накрая.
Пусни програмата, после отвори папката: notes.txt е там и може да се отвори с всеки текстов редактор. Следващото пускане ще го намери.
Във всеки пример по-нататък import-ите са най-горе: от модула взимаш само функциите, които ползваш.
Идеята
Добави и прочети редовете
Разходите се добавят един по един. appendFileSync дописва в края, вместо да записва файла наново. split("\n") разделя прочетения текст на редове:
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("expenses.txt", "Обяд;12.50\n"); appendFileSync("expenses.txt", "Такси;8.20\n"); appendFileSync("expenses.txt", "Книга;18.90\n"); const lines = readFileSync("expenses.txt", "utf8").trimEnd().split("\n"); let total = 0; for (const line of lines) { const parts = line.split(";"); total += Number(parts[1]); console.log(`${parts[0]}: ${parts[1]} €`); } console.log(`${lines.length} разхода, общо ${total.toFixed(2)} €`);} catch (e) { console.log(`Проблем с файла: ${e.message}`);}Обяд: 12.50 €Такси: 8.20 €Книга: 18.90 €3 разхода, общо 39.60 €lines е масив от текстове, по един за всеки ред. trimEnd() преди split маха последния знак за нов ред. Без него масивът би имал накрая още един елемент: празен текст "".
По-точно, за любопитните
appendFileSyncсъздава файла, ако го няма, затова първото добавяне не дава грешка.- Без
"utf8"readFileSyncвръщаBuffer: самите байтове на файла, например<Buffer d0 9f d1 8a ...>. Текст става едва с кодировката. - Има и вариант, при който програмата не стои и не чака диска:
import { readFile } from "node:fs/promises";иconst text = await readFile("expenses.txt", "utf8");. В сървърите се ползва той. В кратки програмиreadFileSyncе по-прост. readFileSyncчете целия файл в паметта. За файл от няколко гигабайта е по-добре да четеш ред по ред. Виж картата за потоците.
Пробвай
Две смени
Какво ще отпечата програмата?
import { readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("shift.txt", "Първа смяна: Иво\n"); writeFileSync("shift.txt", "Втора смяна: Мила\n"); process.stdout.write(readFileSync("shift.txt", "utf8"));} catch (e) { console.log(e.message);}writeFileSync изтрива старото съдържание и записва новото. Първият ред е изчезнал още преди четенето. Това е най-честата причина „файлът ми се изтри“.
Идеята
Когато файла го няма
Настройките на програмата са във config.txt, а някой го е изтрил. Ако никой не хване изключението:
import { readFileSync } from "node:fs";
function readConfig() { return readFileSync("config.txt", "utf8");}
console.log(readConfig());node:fs:440 return binding.readFileUtf8(path, stringToFlags(options.flag)); ^
Error: ENOENT: no such file or directory, open 'config.txt' at readFileSync (node:fs:440:20) at readConfig (file:///app/app.js:4:10) at file:///app/app.js:7:13 ... { errno: -2, code: 'ENOENT', syscall: 'open', path: 'config.txt'}Най-горе Node показва реда от своя вътрешен код, където е хвърлена грешката. После е самата грешка: ENOENT (error, no entry) е кодът за „няма такъв файл или папка“. Редовете с „at“ са отвътре навън: readFileSync, после readConfig на ред 4, извикана от ред 7. Редовете на Node след тях са съкратени с „...“.
Грешките на node:fs са от вида Error и се различават по e.code. Провери кода в catch:
import { existsSync, readFileSync } from "node:fs";
function readConfig() { return readFileSync("config.txt", "utf8");}
try { console.log(readConfig());} catch (e) { if (e.code === "ENOENT") { console.log(`Няма файл ${e.path}. Ползвам настройките по подразбиране.`); } else { console.log(`Не мога да прочета настройките: ${e.message}`); }}console.log(existsSync("config.txt"));Няма файл config.txt. Ползвам настройките по подразбиране.falsee.path е името на файла, който липсва. existsSync(path) проверява дали файлът съществува. Проверката не замества catch: файлът може да изчезне между проверката и четенето, а и има други причини четенето да не стане.
Идеята
Ред по ред и потоци
writeFileSync записва целия текст наведнъж. За писане на части и за четене ред по ред има потоци (streams). Потокът държи файла отворен, докато работиш с него, и затова трябва да се затвори накрая:
import { createReadStream, createWriteStream } from "node:fs";import { once } from "node:events";import { createInterface } from "node:readline";
try { const out = createWriteStream("report.txt"); for (let day = 1; day <= 3; day++) { out.write(`Ден ${day}: ${day * 120} €\n`); } out.end(); await once(out, "close");} catch (e) { console.log(`Не мога да запиша: ${e.message}`);}
try { const lines = createInterface({ input: createReadStream("report.txt") }); for await (const line of lines) { console.log(line); }} catch (e) { console.log(`Не мога да прочета: ${e.message}`);}Ден 1: 120 €Ден 2: 240 €Ден 3: 360 €createWriteStreamотваря файла за писане.writeдава текста на потока. Нов ред сам не идва:\nго пишеш ти.end()казва „това е всичко“ и започва затварянето.await once(out, "close")чака, докато потокът запише всичко и затвори файла.onceе от модулаnode:events.createInterfaceотnode:readlineчете потока от файла ред по ред, аfor awaitдава редовете един по един, без знака за нов ред. Това е същото, с което четеш и от клавиатурата.
Затварянето не е формалност: докато потокът не е затворен, текстът може още да е в буфера му в паметта, а не на диска. За да е кодът кратък, тук няма try:
import { createWriteStream, readFileSync, statSync } from "node:fs";import { once } from "node:events";
const out = createWriteStream("report.txt");for (let day = 1; day <= 3; day++) { out.write(`Ден ${day}: ${day * 120} €\n`);}console.log(`В буфера: ${out.writableLength} байта`);out.end();await once(out, "close");console.log(`Във файла: ${statSync("report.txt").size} байта`);for (const line of readFileSync("report.txt", "utf8").trimEnd().split("\n")) { console.log(line);}Стъпка по стъпка, с думи
- node:fs дава потока и размера на файла, node:events чакането. Отляво е паметта на програмата, отдясно е дискът.
- createWriteStream създава поток с буфер в паметта. Файлът report.txt се отваря, без програмата да чака.
- write дава текста на потока и той чака в буфера. До диска още не стига нищо.
- writableLength: 54 байта чакат в буфера. На диска няма нито един.
- end() затваря потока, а await чака това да стане. Буферът отива на диска: файлът става 54 байта.
- 54 байта за 39 знака с новите редове: в UTF-8 всяка буква на кирилица е 2 байта, а € е 3.
- readFileSync чете трите реда обратно от диска.
В JavaScript няма try (...), който сам затваря ресурс. readFileSync и writeFileSync нямат нужда от него, защото отварят и затварят файла сами. Когато ти държиш файла отворен, затварянето е във finally от урока за finally:
import { closeSync, openSync, writeSync } from "node:fs";
const fd = openSync("report.txt", "w");try { writeSync(fd, "Ден 1: 120 €\n");} finally { closeSync(fd);}openSync отваря файла и връща номера му (fd, file descriptor). writeSync пише направо във файла, без буфер. closeSync го затваря, каквото и да стане в try.
По-точно, за любопитните
- В езика идва
using: ресурс, който се затваря сам в края на блока, кактоtry (...)в Java. Node 22 още не го разпознава и даваSyntaxError. - Буфер. Потокът събира текста в паметта (
out.writableLengthе колко байта чакат) и го праща към диска, без програмата да спира и да чака. Затова следend()е нуженawait. readlineнадcreateReadStreamе начинът за много голям файл: в паметта е само текущото парче. Ето го отделно:
import { createReadStream, writeFileSync } from "node:fs";import { createInterface } from "node:readline";
writeFileSync("report.txt", "Ден 1: 120 €\nДен 2: 240 €\n");const lines = createInterface({ input: createReadStream("report.txt") });for await (const line of lines) { console.log(line);}Пробвай
Кога се затваря?
В JavaScript няма try (...), затова ресурсът се затваря във finally. Ето принтерът на касата от урока за finally: close() е във finally на вътрешния try, а грешката се хваща от външния:
Какво ще отпечата програмата?
class Printer { constructor() { console.log("Принтерът е зает."); }
close() { console.log("Принтерът е свободен."); }}
try { const printer = new Printer(); try { console.log("Печатам бележката..."); const qty = Number("три"); if (Number.isNaN(qty)) { throw new Error("три не е число"); } console.log(`Количество: ${qty}`); } finally { printer.close(); }} catch (e) { console.log("Грешно количество.");}Number("три") дава NaN, без грешка, затова проверката хвърля сама. Редът „Количество“ се прескача. finally на вътрешния try се изпълнява веднага, още преди външния catch. Затова „свободен“ е преди „Грешно количество.“ Когато catch се изпълнява, принтерът вече е освободен.Пробвай
Намери грешката
1, а редовете са два. На кой ред е грешката?Избери реда с грешката.
end() само започва затварянето. Записът става по-късно, а readFileSync чете веднага, когато във файла е само заглавният ред. Програмата не дава грешка, а тихо брои грешно. След out.end(); добави await once(out, "close"); (once е от node:events): тогава потокът е затворен и редовете са два.Идеята
Папки
Резервните копия на касата отиват в папка, по едно на ден:
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";import { join } from "node:path";
const backup = "backup";try { mkdirSync(join(backup, "old"), { recursive: true }); writeFileSync(join(backup, "2026-10-01.csv"), "Кафе,2.40\n"); writeFileSync(join(backup, "2026-10-02.csv"), "Кафе,2.40\nЧай,1.80\n"); for (const name of readdirSync(backup).sort()) { const p = join(backup, name); if (statSync(p).isDirectory()) { console.log(`${name}/`); } else { console.log(`${name} (${statSync(p).size} байта)`); } } unlinkSync(join(backup, "2026-10-01.csv")); console.log(existsSync(join(backup, "2026-10-01.csv")));} catch (e) { console.log(e.message);}2026-10-01.csv (14 байта)2026-10-02.csv (26 байта)old/false| Функция | Какво прави |
|---|---|
mkdirSync(p, { recursive: true }) |
създава папката и всички папки над нея, ако ги няма |
join(dir, "име") (от node:path) |
пътят до име вътре в dir |
statSync(p).isDirectory(), existsSync(p) |
папка ли е, има ли го |
statSync(p).size |
размерът в байтове |
unlinkSync(p) |
изтрива файл (rmdirSync(p) изтрива празна папка) |
readdirSync(dir) |
масив с имената на всичко в папката |
Размерите са 14 и 26 байта, макар че символите са по-малко: всяка буква на кирилица е 2 байта в UTF-8.
readdirSync не обещава ред на имената, затова ги подреждаме със sort().По-точно, за любопитните
rmdirSyncизтрива само празна папка. Иначе хвърля грешка с кодENOTEMPTY.rmSync(p, { recursive: true })изтрива папката с всичко в нея, затова внимавай с него.copyFileSync(от, до)копира файл, аrenameSync(от, до)го мести или преименува.joinслага между частите разделителя на системата:/в Linux и macOS,\в Windows. Затова пътищата не се сглобяват с+.- Пътят
"notes.txt"е относителен: спрямо папката, от която е пусната програмата."/home/ana/notes.txt"е абсолютен.resolve("notes.txt")отnode:pathпоказва къде точно е файлът.
Упражнение
Програми, които помнят
Реши задачите в редактора си. Във всяка задача програмата първо сама създава входния файл с writeFileSync, за да можеш да я пробваш веднага.
1. Конфигурационен файл. Създай server.conf с това съдържание, после го прочети и отпечатай всяка настройка като ключ: стойност. Празните редове и редовете, които започват с #, се пропускат. Подсказка: намери първото = с indexOf и вземи двете части със slice. split("=", 2) тук не помага: в JavaScript той изхвърля всичко след второто =.
# настройки на сървъраmax_connections=500log_file_path=/var/log/server.log
db_timeout=30max_connections: 500log_file_path: /var/log/server.logdb_timeout: 302. Филтър на лог файл. Създай app.log с четири реда (два INFO и два ERROR). Прочети от клавиатурата ниво, например ERROR, и запиши редовете с това ниво в errors.log ред по ред: чети с readline, пиши с поток (createWriteStream). Отпечатай errors.log и колко реда си записал.
ERRORERROR: Failed to load configurationERROR: Null pointer exceptionЗаписах 2 реда в errors.log3. Съдържание на папка. Създай папка project с файловете document.txt, image.jpg, script.js и папката backup. Запиши имената им, подредени по азбучен ред, в directory_contents.txt, като след имената на папките сложиш /. Отпечатай файла.
backup/document.txtimage.jpgscript.js4. Индексен файл. Създай text.txt с трите реда по-долу. Прочети дума от клавиатурата и запиши в index.txt всяка нейна поява във вида ред:позиция: номерът на реда (от 1) и индексът в реда (от 0). Ако думата е два пъти на един ред, това са два записа.
hello worldworld hellohello again, hellohello1:02:63:03:13Още задачи
- Брояч на пусканията. Поправи програмата от първата карта: брой пусканията във
visits.txt. Ако файлът го няма, започни от 0. - Намери и замени във файл. Задачата от урока „Търсене и замяна“: прочети файл, замени „старо“ с „ново“ и запиши резултата в нов файл.
- Дати от файл. Задачата за дати от урока „Регулярни изрази“: чети датите от файл, по една на ред.
- Копие с номерата на редовете. Прочети файл и запиши копие, в което всеки ред започва с номера си:
1: ...,2: ....
Тест