JS: Деревья

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

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

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

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

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

import * as fsTrees from '@hexlet/immutable-fs-trees'

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

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

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

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

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

import * as fsTrees from '@hexlet/immutable-fs-trees'

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

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

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

Обработка

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

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

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

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

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

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

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

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

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

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

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

const children = fsTrees.getChildren(tree)
const newMeta = _.cloneDeep(fsTrees.getMeta(tree))
// reverse изменяет массив, поэтому клонируем
const newChildren = [...children].reverse()
const tree2 = fsTrees.mkdir(fsTrees.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 = fsTrees.mkdir('/', [
  fsTrees.mkfile('oNe'),
  fsTrees.mkfile('Two'),
  fsTrees.mkdir('THREE'),
])

const children = fsTrees.getChildren(tree)
const newChildren = children.map((child) => {
  const name = fsTrees.getName(child)
  const newMeta = _.cloneDeep(fsTrees.getMeta(child))
  if (fsTrees.isDirectory(child)) {
    const children = [...fsTrees.getChildren(child)]
    return fsTrees.mkdir(name.toLowerCase(), children, newMeta)
  }
  return fsTrees.mkfile(name.toLowerCase(), newMeta)
})
// Обязательно копируем метаданные
const newMeta = _.cloneDeep(fsTrees.getMeta(tree))
const tree2 = fsTrees.mkdir(fsTrees.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 = fsTrees.mkdir('/', [
  fsTrees.mkfile('one'),
  fsTrees.mkfile('two'),
  fsTrees.mkdir('three'),
])

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

Рекомендуемые программы