Към съдържанието
wannacode

Файлове

Проблем

Програмата забравя

Пишеш програма за разходите си. Въвеждаш ги вечер, затваряш я, а на другия ден тя не знае нищо за вчера. Ето най-краткият пример:

let visits = 0;
visits++;
console.log(`Пусната ${visits} път.`);
Изход
Пусната 1 път.

Колкото пъти и да я пуснеш, казва „1 път“. Променливите живеят в паметта, докато програмата работи. Когато свърши, паметта се освобождава и всичко изчезва.

За да помни, програмата трябва да запише данните там, където остават и след нея: във файл на диска.

Идеята

Файл с node:fs

Ето цяла програма, която записва бележка във файл и я чете обратно:

notes.js
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);
}
Отговор

Идеята

Когато файла го няма

Настройките на програмата са във config.txt, а някой го е изтрил. Ако никой не хване изключението:

app.js
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. Ползвам настройките по подразбиране.
false

e.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);
}
Стъпка по стъпка, с думи
  1. node:fs дава потока и размера на файла, node:events чакането. Отляво е паметта на програмата, отдясно е дискът.
  2. createWriteStream създава поток с буфер в паметта. Файлът report.txt се отваря, без програмата да чака.
  3. write дава текста на потока и той чака в буфера. До диска още не стига нищо.
  4. writableLength: 54 байта чакат в буфера. На диска няма нито един.
  5. end() затваря потока, а await чака това да стане. Буферът отива на диска: файлът става 54 байта.
  6. 54 байта за 39 знака с новите редове: в UTF-8 всяка буква на кирилица е 2 байта, а € е 3.
  7. 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("Грешно количество.");
}
Отговор

Пробвай

Намери грешката

Програмата добавя поръчка под заглавния ред и брои редовете във файла. Отпечатва 1, а редовете са два. На кой ред е грешката?

Избери реда с грешката.

Идеята

Папки

Резервните копия на касата отиват в папка, по едно на ден:

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 той изхвърля всичко след второто =.

server.conf
# настройки на сървъра
max_connections=500
log_file_path=/var/log/server.log
db_timeout=30
Изход
max_connections: 500
log_file_path: /var/log/server.log
db_timeout: 30

2. Филтър на лог файл. Създай app.log с четири реда (два INFO и два ERROR). Прочети от клавиатурата ниво, например ERROR, и запиши редовете с това ниво в errors.log ред по ред: чети с readline, пиши с поток (createWriteStream). Отпечатай errors.log и колко реда си записал.

Вход
ERROR
Изход
ERROR: Failed to load configuration
ERROR: Null pointer exception
Записах 2 реда в errors.log

3. Съдържание на папка. Създай папка project с файловете document.txt, image.jpg, script.js и папката backup. Запиши имената им, подредени по азбучен ред, в directory_contents.txt, като след имената на папките сложиш /. Отпечатай файла.

directory_contents.txt
backup/
document.txt
image.jpg
script.js

4. Индексен файл. Създай text.txt с трите реда по-долу. Прочети дума от клавиатурата и запиши в index.txt всяка нейна поява във вида ред:позиция: номерът на реда (от 1) и индексът в реда (от 0). Ако думата е два пъти на един ред, това са два записа.

text.txt
hello world
world hello
hello again, hello
Вход
hello
index.txt
1:0
2:6
3:0
3:13

Още задачи

  • Брояч на пусканията. Поправи програмата от първата карта: брой пусканията във visits.txt. Ако файлът го няма, започни от 0.
  • Намери и замени във файл. Задачата от урока „Търсене и замяна“: прочети файл, замени „старо“ с „ново“ и запиши резултата в нов файл.
  • Дати от файл. Задачата за дати от урока „Регулярни изрази“: чети датите от файл, по една на ред.
  • Копие с номерата на редовете. Прочети файл и запиши копие, в което всеки ред започва с номера си: 1: ..., 2: ....

Тест

Провери се

1. Какво връща readFileSync("notes.txt") без "utf8"?
2. writeFileSync(file, "А\n"); после writeFileSync(file, "Б\n"); Какво има във файла?
3. Има ли в JavaScript (Node 22) try (...), който сам затваря файла?
4. readFileSync не намира файла. Как разбираш, че точно това е проблемът?
5. Пишеш с createWriteStream, викаш end() и веднага четеш файла с readFileSync. Какво може да има в него?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·