hexlet-courses
October 1, 2020

4. Манипуляции с виртуальной файловой системой

Библиотека, которая используется для построения деревьев, рассчитана только на неизменяемые файловые структуры. То есть уже после создания её поменять нельзя. Но можно на основе старой структуры сделать новую, в которой какие-то части будут изменены.

Неизменяемая структура выбрана для этого курса не случайно. Такую структуру легче отлаживать и меньше шансов допустить ошибки. И она позволяет максимально погрузиться в использование функций высшего порядка.

Базовые операции с узлами

Пакет @hexlet/immutable-fs-trees позволяет не только создавать, но и извлекать данные из уже созданных файлов и директорий. Они позволяют не лезть во внутреннюю структуру самого дерева:

import {
  mkfile, mkdir, getChildren, getMeta, getName,
} from '@hexlet/immutable-fs-trees';

const tree = mkdir('/', [mkfile('hexlet.log')], { hidden: true });
getName(tree); // '/'
getMeta(tree).hidden; // true

const [file] = getChildren(tree);
getName(file); // 'hexlet.log'

// У файла нет метаданных
getMeta(file).unknown; // undefined

// А вот так делать не надо
// У файлов нет детей
getChildren(file);

Дополнительно в пакете есть две функции для проверки типа. С их помощью можно выборочно работать с файлами и директориями:

import {
  mkfile, mkdir, isFile, isDirectory, getChildren,
} from '@hexlet/immutable-fs-trees';

const tree = mkdir('/', [mkfile('hexlet.log')], { hidden: true });
isDirectory(tree); // true
isFile(tree); // false

const [file] = getChildren(tree);
isFile(file); // true
isDirectory(file); // false

Рассмотренных операций хватит для выполнения любых преобразований над файлами и директориями. Начнём с самых простых, которые не требуют рекурсивного обхода.

Обработка

Любая обработка в неизменяемом стиле сводится к формированию новых данных на основе старых. Ниже мы реализуем некоторые варианты преобразования, раскрывающие эту идею.

Изменение имени файла

const file = mkfile('one', { size: 35 });

// При переименовании важно сохранить метаданные
// _ – lodash
const newMeta = _.cloneDeep(getMeta(file));
const newFile = mkfile('new name', newMeta);

Фактически здесь создаётся новый файл с метаданными старого. Перед тем как создать новый файл, метаданные клонируются (глубоким клонированием). Почему? Объекты передаются по ссылке, и если не выполнить клонирование, то в метаданных нового файла окажутся метаданные старого. Как только мы захотим изменить что-то, то изменив новое — сломаем старое:

const file = mkfile('one', { size: 35 });

// При переименовании важно сохранить метаданные
const newMeta = getMeta(file);
// Бум! У file тоже поменялись метаданные
newMeta.size = 15;
const newFile = mkfile('new name', newMeta);

console.log(getMeta(file)); // { size: 15 }

Сортировка содержимого директории

// Сортировка в обратном порядке

const tree = mkdir('/', [
  mkfile('one'),
  mkfile('two'),
  mkdir('three'),
]);

const children = getChildren(tree);
const newMeta = _.cloneDeep(getMeta(tree));
// reverse изменяет массив, поэтому клонируем
const newChildren = children.slice().reverse();
const tree2 = mkdir(getName(tree), newChildren, newMeta);
console.log(tree2);
// => {
// =>   name: '/',
// =>   children: [
// =>     { name: 'three', children: [], meta: {}, type: 'directory' },
// =>     { name: 'two', meta: {}, type: 'file' },
// =>     { name: 'one', meta: {}, type: 'file' }
// =>   ],
// =>   meta: {},
// =>   type: 'directory'
// => }

Обновление содержимого директории

// Приведение к нижнему регистру имён директорий и файлов
// внутри конкретной директории

const tree = mkdir('/', [
  mkfile('oNe'),
  mkfile('Two'),
  mkdir('THREE'),
]);

const children = getChildren(tree);
const newChildren = children.map((child) => {
  const name = getName(child);
  const newMeta = _.cloneDeep(getMeta(child));
  if (isDirectory(child)) {
    return mkdir(name.toLowerCase(), getChildren(child), newMeta);
  }
  return mkfile(name.toLowerCase(), newMeta);
});
// Обязательно копируем метаданные
const newMeta = _.cloneDeep(getMeta(tree));
const tree2 = mkdir(getName(tree), newChildren, newMeta);
console.log(tree2);
// => {
// =>   name: '/',
// =>   children: [
// =>     { name: 'one', meta: {}, type: 'file' },
// =>     { name: 'two', meta: {}, type: 'file' },
// =>     { name: 'three', children: [], meta: {}, type: 'directory' }
// =>   ],
// =>   meta: {},
// =>   type: 'directory'
// => }

Удаление файлов внутри директории

const tree = mkdir('/', [
  mkfile('one'),
  mkfile('two'),
  mkdir('three'),
]);

const children = getChildren(tree);
const newChildren = children.filter(isDirectory);
const newMeta =  _.cloneDeep(getMeta(tree));
const tree2 = mkdir(getName(tree), newChildren, newMeta);
console.log(tree2);
// => {
// =>   name: '/',
// =>   children: [ { name: 'three', children: [], meta: {}, type: 'directory' } ],
// =>   meta: {},
// =>   type: 'directory'
// => }

Тесты

Вопрос 1

Как isDirectory проверяет что директория это директория?

Через проверку того что перед нами не файл
Смотрит наличие children
Через сравнение типа со строкой 'directory'

Вопрос 2

Могут ли на одном уровне находиться директории и файлы в перемешку?

Нет, в директории могут находиться либо только другие директории, либо только файлы
Могут, но только внутри корневой директории
Да

Вопрос 3

Каким образом в файловом дереве обновляется файл?

Создается новый на основе старого
С помощью магии
Так как узел это объект, он просто изменяется напрямую

Упражнение

tree.js

Реализуйте и экспортируйте функцию compressImages(), которая принимает на вход директорию, находит внутри нее картинки и "сжимает" их. Под сжиманием понимается уменьшение свойства size в метаданных в два раза. Функция должна вернуть обновленную директорию со сжатыми картинками и всеми остальными данными, которые были внутри этой директории.

Картинками считаются все файлы заканчивающиеся на .jpg.

Примеры

const tree = mkdir('my documents', [
  mkfile('avatar.jpg', { size: 100 }),
  mkfile('passport.jpg', { size: 200 }),
  mkfile('family.jpg', { size: 150 }),
  mkfile('addresses', { size: 125 }),
  mkdir('presentations')
]);

const newTree = compressImages(tree);
// То же самое, что и tree, но во всех картинках размер уменьшен в два раза

tree.js file

/* eslint-disable import/prefer-default-export */// @ts-check
import _ from 'lodash';import {  mkdir, mkfile, isFile, getChildren, getName, getMeta,} from '@hexlet/immutable-fs-trees';
// BEGIN (write your solution here)
// END

Хештеги