Как получить список файлов в Node.js: методы, примеры, оптимизация
Для кого эта статья:
- Node.js-разработчики, желающие улучшить свои навыки работы с файловой системой.
- Студенты и начинающие веб-разработчики, изучающие Node.js и его функционал.
Практикующие разработчики, которые ищут оптимальные методы обработки файлов в своих приложениях.
Работа с файлами — хлеб насущный для любого Node.js-разработчика. Получить список файлов в директории — казалось бы, простая задача, но стоит копнуть глубже, и мы обнаружим целый арсенал методов и подходов, каждый со своими нюансами. От классического
fs.readdirдо современных promise-based API — Node.js предлагает множество инструментов для эффективного взаимодействия с файловой системой. В этой статье мы разберем все ключевые методы получения списка файлов, их преимущества, особенности применения и приведем практические примеры для типичных сценариев. 💻
Если вам интересно не просто узнать о методах работы с файлами, но и научиться создавать полноценные веб-приложения на Node.js, обратите внимание на Обучение веб-разработке от Skypro. Курс построен на практических задачах — вы освоите не только файловые операции, но и полный стек технологий для создания современных веб-сервисов. Преподаватели-практики помогут избежать типичных ошибок и научат писать эффективный и поддерживаемый код.
Основные методы Node.js для получения списка файлов
Node.js предоставляет несколько способов получения списка файлов в директории через встроенный модуль fs (file system). Понимание различных API и их особенностей поможет выбрать оптимальный подход для конкретной задачи.
Рассмотрим основные методы:
| Метод | Тип | Особенности | Версия Node.js |
|---|---|---|---|
| fs.readdir | Асинхронный с колбэками | Не блокирует поток выполнения | Все версии |
| fs.readdirSync | Синхронный | Блокирует поток до завершения операции | Все версии |
| fs.promises.readdir | Асинхронный с промисами | Современный подход с поддержкой async/await | 10.0.0+ |
| Библиотека glob | Зависит от реализации | Поддержка паттернов для фильтрации | Внешний пакет |
Прежде чем мы погрузимся в детали, важно понимать базовую структуру путей в Node.js. Для обработки путей рекомендуется использовать модуль path, который обеспечивает кроссплатформенную совместимость:
const path = require('path');
// Построение абсолютного пути к директории
const directoryPath = path.join(__dirname, 'target_directory');
Выбор метода зависит от нескольких факторов:
- Сложность приложения — для небольших скриптов подойдет синхронный подход, для высоконагруженных приложений — асинхронный
- Структура кода — современные приложения часто используют async/await, что делает промисы предпочтительнее колбэков
- Требования к обработке ошибок — промисы обеспечивают более элегантный способ обработки ошибок
- Версия Node.js — не все API доступны в старых версиях
Теперь давайте подробнее рассмотрим каждый подход, начиная с классических методов.

Синхронный и асинхронный подходы с fs.readdir
Node.js предоставляет как синхронные, так и асинхронные методы для работы с файловой системой. Разница между ними критична для производительности приложения. 🚀
Начнем с асинхронного метода fs.readdir. Он не блокирует событийный цикл Node.js, что критически важно для поддержания отзывчивости приложения:
const fs = require('fs');
const path = require('path');
const directoryPath = path.join(__dirname, 'example');
fs.readdir(directoryPath, (err, files) => {
if (err) {
return console.log('Ошибка при чтении директории: ' + err);
}
console.log('Файлы в директории:');
files.forEach(file => {
console.log(file);
});
});
console.log('Это выполнится до завершения чтения директории!');
Синхронный вариант fs.readdirSync блокирует выполнение до завершения операции чтения директории:
const fs = require('fs');
const path = require('path');
const directoryPath = path.join(__dirname, 'example');
try {
const files = fs.readdirSync(directoryPath);
console.log('Файлы в директории:');
files.forEach(file => {
console.log(file);
});
} catch (err) {
console.log('Ошибка при чтении директории: ' + err);
}
console.log('Это выполнится только после завершения чтения директории');
Максим Петров, lead-разработчик бэкенда
На старте карьеры я столкнулся с неприятным инцидентом в production. Мы разрабатывали сервис для обработки пользовательских файлов, и я использовал fs.readdirSync для каталога с тысячами файлов. В час пик наш сервис начал "падать" по таймауту. Диагностика показала, что синхронная операция блокировала event loop на несколько секунд, что приводило к отказу в обслуживании новых запросов.
После рефакторинга на fs.readdir с правильной обработкой очередей задач, производительность выросла в 8 раз, а таймауты исчезли. Этот опыт научил меня относиться к выбору синхронных/асинхронных методов не как к вопросу удобства кода, а как к критическому архитектурному решению.
Вот основные отличия этих подходов:
| Характеристика | fs.readdir (асинхронный) | fs.readdirSync (синхронный) |
|---|---|---|
| Блокировка потока | Не блокирует | Блокирует до завершения |
| Обработка результата | Через колбэк | Возвращает значение напрямую |
| Обработка ошибок | Первый аргумент колбэка | Try-catch блок |
| Подходит для | Сервисы, API, интерактивные приложения | Скрипты, утилиты, инициализация |
| Производительность при большом количестве файлов | Высокая | Низкая (блокирует event loop) |
Оба метода принимают дополнительный параметр options, который позволяет настроить формат возвращаемых данных:
fs.readdir(directoryPath, { withFileTypes: true }, (err, files) => {
if (err) throw err;
files.forEach(dirent => {
if (dirent.isFile()) {
console.log(`${dirent.name} – это файл`);
} else if (dirent.isDirectory()) {
console.log(`${dirent.name} – это директория`);
}
});
});
Опция withFileTypes возвращает массив объектов Dirent вместо массива строк, что позволяет получить дополнительную информацию о каждом элементе без выполнения дополнительных операций stat.
Выбирая между синхронным и асинхронным подходами, руководствуйтесь следующими принципами:
- Используйте синхронные методы только при инициализации приложения или в простых скриптах
- В веб-серверах, API и других высоконагруженных сервисах всегда используйте асинхронные методы
- Если требуется последовательная обработка файлов, используйте асинхронные методы в сочетании с async/await или библиотеками для управления потоком выполнения
Работа с fs.promises для современного кода
С версии Node.js 10.0.0 появился модуль fs.promises, который предоставляет API на основе промисов для работы с файловой системой. Этот подход значительно улучшает читаемость кода и упрощает обработку ошибок по сравнению с традиционными колбэками. 🔄
Базовый пример получения списка файлов с использованием fs.promises:
const fs = require('fs').promises;
const path = require('path');
async function getFilesList(directoryPath) {
try {
const files = await fs.readdir(directoryPath);
console.log('Файлы в директории:');
files.forEach(file => {
console.log(file);
});
return files;
} catch (err) {
console.error('Ошибка при чтении директории:', err);
throw err;
}
}
// Использование функции
getFilesList(path.join(__dirname, 'example'))
.then(files => console.log(`Всего файлов: ${files.length}`))
.catch(err => console.error('Произошла ошибка:', err));
Преимущества использования fs.promises по сравнению с традиционным подходом:
- Упрощение логики асинхронного кода — возможность использовать async/await делает код более линейным и понятным
- Эффективная обработка ошибок — используя конструкцию try-catch вместо проверки ошибки в колбэке
- Простота композиции — легко комбинировать несколько асинхронных операций
- Лучшая поддержка параллельного выполнения — с помощью Promise.all можно обрабатывать группы файлов параллельно
Для получения расширенной информации о файлах также можно использовать опцию withFileTypes:
const fs = require('fs').promises;
const path = require('path');
async function getFilesDetails(directoryPath) {
try {
const dirents = await fs.readdir(directoryPath, { withFileTypes: true });
const files = dirents
.filter(dirent => dirent.isFile())
.map(dirent => dirent.name);
const directories = dirents
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
return { files, directories };
} catch (error) {
console.error('Ошибка при чтении директории:', error);
throw error;
}
}
getFilesDetails(path.join(__dirname, 'example'))
.then(result => {
console.log('Файлы:', result.files);
console.log('Директории:', result.directories);
})
.catch(err => console.error('Ошибка:', err));
Для получения подробной информации о каждом файле можно комбинировать readdir и stat:
const fs = require('fs').promises;
const path = require('path');
async function getFilesWithStats(directoryPath) {
try {
const files = await fs.readdir(directoryPath);
const fileStats = await Promise.all(
files.map(async filename => {
const filePath = path.join(directoryPath, filename);
const stats = await fs.stat(filePath);
return {
name: filename,
size: stats.size,
isDirectory: stats.isDirectory(),
created: stats.birthtime,
modified: stats.mtime
};
})
);
return fileStats;
} catch (error) {
console.error('Ошибка при чтении файлов:', error);
throw error;
}
}
getFilesWithStats(path.join(__dirname, 'example'))
.then(filesInfo => console.log(filesInfo))
.catch(err => console.error('Ошибка:', err));
Использование промисов особенно удобно при последовательной обработке файлов или когда необходимо выполнить дополнительные асинхронные операции для каждого файла. Например, чтение содержимого каждого файла:
const fs = require('fs').promises;
const path = require('path');
async function readAllTextFiles(directoryPath) {
try {
const files = await fs.readdir(directoryPath);
const textFiles = files.filter(file => file.endsWith('.txt'));
const contents = await Promise.all(
textFiles.map(async file => {
const content = await fs.readFile(path.join(directoryPath, file), 'utf8');
return { name: file, content };
})
);
return contents;
} catch (error) {
console.error('Ошибка при чтении файлов:', error);
throw error;
}
}
readAllTextFiles(path.join(__dirname, 'example'))
.then(filesContents => {
filesContents.forEach(file => {
console.log(`Файл: ${file.name}`);
console.log(`Содержимое: ${file.content}`);
console.log('---');
});
})
.catch(err => console.error('Ошибка:', err));
Алексей Сорокин, архитектор Node.js-приложений
Когда мы переписывали микросервис обработки изображений для крупного фотохостинга, замена колбэков на промисы привела к неожиданному результату. Код не только стал чище и понятнее, но и количество ошибок при деплое снизилось на 43%.
Самым показательным кейсом был рекурсивный обход директорий с изображениями: с колбэками код превращался в "ёлочку" из вложенных функций, а обработка ошибок дублировалась на каждом уровне. Переход на fs.promises с async/await сократил код на 30% и сделал отлов ошибок централизованным. Важный урок: современный синтаксис — это не просто "сахар", а инструмент повышения надежности.
Фильтрация и обработка полученных файлов
Получение списка файлов часто является лишь первым шагом. Типичные задачи включают фильтрацию по расширениям, сортировку, группировку и другие операции обработки. Рассмотрим наиболее распространенные подходы к фильтрации и обработке файлов. 🔍
Начнем с базовой фильтрации по расширению файла:
const fs = require('fs').promises;
const path = require('path');
async function getFilesByExtension(directoryPath, extension) {
try {
const files = await fs.readdir(directoryPath);
// Фильтруем файлы по расширению
return files.filter(file => path.extname(file).toLowerCase() === `.${extension.toLowerCase()}`);
} catch (error) {
console.error('Ошибка при чтении директории:', error);
throw error;
}
}
// Получение всех JavaScript файлов
getFilesByExtension(path.join(__dirname, 'project'), 'js')
.then(jsFiles => console.log('JavaScript файлы:', jsFiles))
.catch(err => console.error('Ошибка:', err));
Для более сложной фильтрации, например, по шаблонам (glob patterns), можно использовать специализированные библиотеки, такие как glob или fast-glob:
const glob = require('glob');
const util = require('util');
const globPromise = util.promisify(glob);
async function findFilesByPattern(pattern) {
try {
// Пример шаблона: './src/**/*.js'
return await globPromise(pattern);
} catch (error) {
console.error('Ошибка при поиске файлов:', error);
throw error;
}
}
// Найти все .js файлы во всех подпапках src
findFilesByPattern('./src/**/*.js')
.then(files => console.log('Найденные JS файлы:', files))
.catch(err => console.error('Ошибка:', err));
Часто требуется сортировка файлов по различным критериям:
const fs = require('fs').promises;
const path = require('path');
async function getSortedFiles(directoryPath, sortBy = 'name') {
try {
const files = await fs.readdir(directoryPath);
// Получаем статистику для каждого файла
const filesWithStats = await Promise.all(
files.map(async filename => {
const filePath = path.join(directoryPath, filename);
const stats = await fs.stat(filePath);
return {
name: filename,
path: filePath,
size: stats.size,
created: stats.birthtime,
modified: stats.mtime
};
})
);
// Сортируем по выбранному критерию
switch (sortBy) {
case 'size':
return filesWithStats.sort((a, b) => a.size – b.size);
case 'created':
return filesWithStats.sort((a, b) => a.created – b.created);
case 'modified':
return filesWithStats.sort((a, b) => a.modified – b.modified);
case 'name':
default:
return filesWithStats.sort((a, b) => a.name.localeCompare(b.name));
}
} catch (error) {
console.error('Ошибка при получении или сортировке файлов:', error);
throw error;
}
}
// Получить файлы, отсортированные по размеру (от меньшего к большему)
getSortedFiles(path.join(__dirname, 'documents'), 'size')
.then(files => {
console.log('Файлы по размеру:');
files.forEach(file => {
console.log(`${file.name} – ${(file.size / 1024).toFixed(2)} KB`);
});
})
.catch(err => console.error('Ошибка:', err));
Группировка файлов по типам или другим критериям также является распространенной задачей:
const fs = require('fs').promises;
const path = require('path');
async function groupFilesByType(directoryPath) {
try {
const files = await fs.readdir(directoryPath);
// Объект для группировки файлов
const groupedFiles = {};
files.forEach(file => {
// Получаем расширение файла
const ext = path.extname(file).toLowerCase().replace('.', '') || 'unknown';
// Инициализируем массив для этого типа, если еще не существует
if (!groupedFiles[ext]) {
groupedFiles[ext] = [];
}
// Добавляем файл в соответствующую группу
groupedFiles[ext].push(file);
});
return groupedFiles;
} catch (error) {
console.error('Ошибка при группировке файлов:', error);
throw error;
}
}
groupFilesByType(path.join(__dirname, 'uploads'))
.then(groups => {
console.log('Файлы по типам:');
Object.entries(groups).forEach(([type, files]) => {
console.log(`${type} (${files.length}): ${files.join(', ')}`);
});
})
.catch(err => console.error('Ошибка:', err));
Для анализа и трансформации содержимого файлов можно комбинировать получение списка с чтением:
const fs = require('fs').promises;
const path = require('path');
async function findStringInFiles(directoryPath, searchString) {
try {
const files = await fs.readdir(directoryPath);
const textFiles = files.filter(file =>
['.txt', '.md', '.js', '.html', '.css'].includes(path.extname(file).toLowerCase())
);
const results = [];
for (const file of textFiles) {
const filePath = path.join(directoryPath, file);
const content = await fs.readFile(filePath, 'utf8');
if (content.includes(searchString)) {
results.push({
file,
path: filePath,
occurrences: (content.match(new RegExp(searchString, 'g')) || []).length
});
}
}
return results;
} catch (error) {
console.error('Ошибка при поиске в файлах:', error);
throw error;
}
}
// Поиск строки "TODO" во всех текстовых файлах
findStringInFiles(path.join(__dirname, 'project'), 'TODO')
.then(results => {
console.log(`Найдено ${results.length} файлов с "TODO":`);
results.forEach(result => {
console.log(`${result.file} – ${result.occurrences} вхождений`);
});
})
.catch(err => console.error('Ошибка:', err));
При обработке большого количества файлов стоит учитывать ограничения операционной системы и использовать потоковую обработку или разбивать операции на части:
const fs = require('fs').promises;
const path = require('path');
async function processBatchedFiles(directoryPath, batchSize = 10, processor) {
try {
const allFiles = await fs.readdir(directoryPath);
const results = [];
// Обработка файлов по частям
for (let i = 0; i < allFiles.length; i += batchSize) {
const batch = allFiles.slice(i, i + batchSize);
// Параллельная обработка файлов в пакете
const batchResults = await Promise.all(
batch.map(async file => {
const filePath = path.join(directoryPath, file);
return await processor(filePath, file);
})
);
results.push(...batchResults);
// Даем event loop "отдышаться" между пакетами
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
} catch (error) {
console.error('Ошибка при пакетной обработке:', error);
throw error;
}
}
// Пример функции-обработчика: получение размера файла
async function getFileSize(filePath, fileName) {
try {
const stats = await fs.stat(filePath);
return {
name: fileName,
size: stats.size
};
} catch (error) {
return {
name: fileName,
error: error.message
};
}
}
// Использование
processBatchedFiles(path.join(__dirname, 'large_directory'), 20, getFileSize)
.then(results => console.log(`Обработано ${results.length} файлов`))
.catch(err => console.error('Ошибка:', err));
Практические решения типичных задач с директориями
Теперь, когда мы рассмотрели основные методы получения и обработки файлов, давайте обратимся к практическим решениям для распространенных задач, возникающих при работе с директориями в Node.js проектах. 🛠️
Начнем с рекурсивного обхода директорий — одной из самых востребованных задач:
const fs = require('fs').promises;
const path = require('path');
async function walkDirectory(dir, fileList = []) {
const files = await fs.readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
// Рекурсивно обходим вложенную директорию
fileList = await walkDirectory(filePath, fileList);
} else {
// Добавляем файл в список
fileList.push(filePath);
}
}
return fileList;
}
// Пример использования
walkDirectory(path.join(__dirname, 'project'))
.then(files => {
console.log(`Найдено ${files.length} файлов:`);
files.forEach(file => console.log(file));
})
.catch(err => console.error('Ошибка при обходе директорий:', err));
Для создания и управления временными директориями — частая задача при работе с загрузкой файлов:
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const crypto = require('crypto');
async function createTempDirectory() {
// Создаем уникальное имя для временной директории
const tempDirName = crypto.randomBytes(8).toString('hex');
const tempPath = path.join(os.tmpdir(), tempDirName);
try {
await fs.mkdir(tempPath);
console.log(`Создана временная директория: ${tempPath}`);
return tempPath;
} catch (error) {
console.error('Ошибка при создании временной директории:', error);
throw error;
}
}
async function cleanupTempFiles(directory) {
try {
const files = await fs.readdir(directory);
// Удаляем все файлы в директории
for (const file of files) {
const filePath = path.join(directory, file);
await fs.unlink(filePath);
}
// Удаляем саму директорию
await fs.rmdir(directory);
console.log(`Временная директория очищена: ${directory}`);
} catch (error) {
console.error('Ошибка при очистке временных файлов:', error);
throw error;
}
}
// Пример использования
async function processTempFiles() {
let tempDir = null;
try {
// Создаем временную директорию
tempDir = await createTempDirectory();
// Создаем тестовый файл
const testFilePath = path.join(tempDir, 'test.txt');
await fs.writeFile(testFilePath, 'Тестовые данные');
// Обрабатываем файлы...
console.log('Обработка файлов завершена');
} finally {
// Очищаем временные файлы даже в случае ошибки
if (tempDir) {
await cleanupTempFiles(tempDir);
}
}
}
processTempFiles().catch(err => console.error('Ошибка:', err));
Для отслеживания изменений в директории — полезно при разработке приложений с горячей перезагрузкой:
const fs = require('fs');
const path = require('path');
function watchDirectory(directory, onChange) {
console.log(`Отслеживание изменений в директории: ${directory}`);
// Настраиваем отслеживание с параметрами
const watcher = fs.watch(directory, { recursive: true }, (eventType, filename) => {
if (filename) {
console.log(`Событие: ${eventType}, файл: ${filename}`);
onChange(eventType, filename, path.join(directory, filename));
}
});
// Обработка завершения отслеживания
watcher.on('error', error => {
console.error('Ошибка при отслеживании директории:', error);
});
return {
stop: () => {
watcher.close();
console.log('Отслеживание директории остановлено');
}
};
}
// Пример использования
const stopWatching = watchDirectory(path.join(__dirname, 'src'), (eventType, filename, fullPath) => {
if (path.extname(filename) === '.js') {
console.log(`Обнаружено изменение в JavaScript файле: ${filename}`);
// Выполняем нужные действия при изменении JS файлов
}
});
// Остановка отслеживания через 5 минут
setTimeout(() => {
stopWatching.stop();
}, 5 * 60 * 1000);
Для работы с большим количеством файлов, используя потоковую обработку:
const fs = require('fs');
const path = require('path');
const { Transform } = require('stream');
// Создаем поток для чтения директории
function createDirectoryStream(directory) {
const files = fs.readdirSync(directory);
let index = 0;
// Создаем поток чтения
const stream = new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
callback(null, chunk);
},
flush(callback) {
callback();
}
});
// Функция для отправки следующего файла в поток
function pushNext() {
if (index < files.length) {
const filename = files[index++];
const filePath = path.join(directory, filename);
stream.push({ filename, path: filePath });
// Планируем отправку следующего файла
setImmediate(pushNext);
} else {
// Все файлы отправлены, закрываем поток
stream.push(null);
}
}
// Запускаем отправку файлов
setImmediate(pushNext);
return stream;
}
// Пример использования
function processLargeDirectory(directory) {
let processedCount = 0;
createDirectoryStream(directory)
.on('data', fileInfo => {
processedCount++;
console.log(`Обработка файла: ${fileInfo.filename}`);
// Здесь можно выполнять любую обработку файла
})
.on('end', () => {
console.log(`Обработка завершена. Всего файлов: ${processedCount}`);
})
.on('error', err => {
console.error('Ошибка при обработке директории:', err);
});
}
processLargeDirectory(path.join(__dirname, 'large_directory'));
Для создания автоматического бэкапа директорий:
const fs = require('fs');
const path = require('path');
const archiver = require('archiver'); // Требуется установить: npm install archiver
function backupDirectory(sourceDir, backupPath) {
// Создаем файл для архива
const output = fs.createWriteStream(backupPath);
const archive = archiver('zip', {
zlib: { level: 9 } // Максимальная компрессия
});
// Обработка событий
output.on('close', () => {
const sizeInMB = (archive.pointer() / 1024 / 1024).toFixed(2);
console.log(`Бэкап завершен. Размер архива: ${sizeInMB} MB`);
});
archive.on('warning', err => {
if (err.code === 'ENOENT') {
console.warn('Предупреждение:', err);
} else {
throw err;
}
});
archive.on('error', err => {
throw err;
});
// Связываем архиватор с файлом вывода
archive.pipe(output);
// Добавляем директорию в архив
archive.directory(sourceDir, false);
// Завершаем архивацию
archive.finalize();
}
// Пример использования
function createDateBasedBackup(sourceDir, backupDir) {
const now = new Date();
const timestamp = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}_${now.getHours().toString().padStart(2, '0')}-${now.getMinutes().toString().padStart(2, '0')}`;
const backupFileName = `backup_${timestamp}.zip`;
const backupPath = path.join(backupDir, backupFileName);
// Убеждаемся, что директория для бэкапов существует
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
backupDirectory(sourceDir, backupPath);
}
// Создаем бэкап каталога проекта
createDateBasedBackup(
path.join(__dirname, 'project'),
path.join(__dirname, 'backups')
);
Ключевые моменты, которые стоит учитывать при работе с директориями в Node.js:
- Производительность — для больших директорий всегда используйте асинхронные методы и избегайте блокировки event loop
- Обработка ошибок — файловая система непредсказуема, всегда имейте план действий при возникновении ошибок
- Кросс-платформенность — используйте модуль path для обеспечения совместимости между разными ОС
- Ограничения ОС — учитывайте лимиты на количество одновременно открытых файлов и используйте потоковую обработку или пакетные методы
- Тестирование — создавайте тесты, которые эмулируют файловую систему (например, с помощью mock-fs), чтобы избежать проблем в production
Мы рассмотрели основные методы получения списка файлов в директориях с использованием Node.js — от базовых синхронных операций до продвинутых асинхронных подходов с промисами. Помните: выбор правильного метода зависит от конкретной задачи. Для небольших утилит подойдет fs.readdirSync, для высоконагруженных приложений — fs.promises.readdir с async/await. При обработке больших директорий не забывайте о потоковой обработке и пакетных операциях. Эффективная работа с файловой системой — один из ключевых навыков, отличающих профессионального Node.js-разработчика.