Глава 10 Файловая система

В этой главе:

□ реализация нормализатора пути к файлу;

□ получение канонических путей к файлам из относительных путей;

□ составление списка всех файлов в каталоге;

□ реализация средства поиска текста в стиле

grep
;

□ реализация автоматического средства для переименования файлов;

□ реализация счетчика использования диска;

□ вычисление статистики о типах файлов;

□ реализация средства, которое уменьшает размер папки путем замены дубликатов символьными ссылками.

Введение

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

Одни пути файлов являются абсолютными, другие — относительными, и, возможно, даже не являются прямыми, поскольку содержат косвенные адреса:

.
(текущий каталог) и
..
(родительский каталог). В то же время в различных операционных системах для разделения каталогов используется слеш
/
(Linux, MacOS и различные системы UNIX) или обратный слеш
\
(Windows). И конечно же, существуют разные типы файлов.

Поскольку каждая вторая программа, связанная с работой в файловой системе, нуждается в таком функционале, было бы здорово иметь новую библиотеку файловой системы в C++17 STL. Самое лучшее в данной ситуации то, что принцип работы одинаков для разных операционных систем, поэтому не требуется писать разные фрагменты кода для версий программ, поддерживающих разные операционные системы.

В этой главе мы сначала рассмотрим принцип работы класса

path
, поскольку он выступает самым важным элементом библиотеки. Затем увидим, насколько мощными, но простыми в использовании являются классы
directory_iterator
и
recursive_directory_iterator
при работе с файлами. В конце главы задействуем в примерах маленькие и простые инструменты, которые выполняют реальные задачи, связанные с файловой системой. С этого момента можно будет легко создавать более сложные инструменты.

Реализуем нормализатор пути файла

Мы начинаем эту главу с очень простого примера, иллюстрирующего работу класса

std::filesystem::path
и вспомогательной функции, которая рационально нормализует пути к файлам.

Результатом данного примера является небольшое приложение, которое принимает любой путь к файлу и возвращает его в нормализованной форме. Нормализованный путь к файлу — абсолютный путь к файлу, не содержащий косвенных адресов

.
или
..
.

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


Как это делается

В этом примере мы реализуем программу, которая всего лишь принимает путь к файлу как аргумент командной строки, а затем отображает его в нормализованной форме.


1. Файл с кодом начинается с директив

include
, а затем мы объявляем, что используем пространства имен
std
и
filesystem
:


#include 

#include 


using namespace std;

using namespace filesystem;


2. В функции

main
проверяем, предоставил ли пользователь аргумент командной строки. В случае отрицательного ответа выдаем сообщение об ошибке и отображаем на экране, как правильно работать с программой. Если путь к файлу все же был предоставлен, то создаем на его основе экземпляр объекта
filesystem::path
:


int main(int argc, char *argv[])

{

  if (argc != 2) {

   cout << "Usage: " << argv[0] << " \n";

   return 1;

  }

  const path dir {argv[1]};


3. Поскольку можно создавать объекты класса

path
из любой строки, нельзя быть уверенными в том, что полученный путь к файлу существует в файловой системе компьютера. Чтобы в этом убедиться, можем использовать функцию
filesystem::exists
. Если данного пути к файлу не существует, то просто снова отображаем сообщение об ошибке:


  if (!exists(dir)) {

   cout << "Path " << dir << " does not exist.\n";

   return 1;

  }


4. Итак, на данном этапе мы вполне уверены: пользователь предоставил существующий путь к файлу, зная, что мы можем запросить нормализованную версию, которую затем отобразим. Функция

filesystem::canonical
возвращает другой объект класса
path
. Мы можем вывести на экран его напрямую, но перегруженный оператор
<<
класса
path
берет в кавычки пути к файлу. Чтобы этого избежать, можем отобразить путь к файлу с помощью методов
.c_str()
или
.string()
:


  cout << canonical(dir).c_str() << '\n';

}


5. Скомпилируем программу и поработаем с ней. Когда мы запустим ее в домашнем каталоге и передадим относительный путь к файлу

"src"
, она выведет на экран полный абсолютный путь к файлу.


$ ./normalizer src

/Users/tfc/src


6. Когда мы снова запускаем программу в домашнем каталоге, но даем ей запутанное относительное описание пути к файлу, в котором сначала прописывается вход в папку Desktop, потом прописывается выход из нее с помощью косвенного адреса

..
, затем входим в папку Documents и выходим из нее, чтобы в конечном итоге попасть в каталог
src
, программа отображает тот же путь файла, что и ранее!


$ ./normalizer Desktop/../Documents/../src

/Users/tfc/src


Как это работает

Этот начальный пример работы с

std::filesystem
мы сделали относительно коротким и прямолинейным. Мы инициализировали объект класса path на основе строки, которая содержит описание пути к файлу. Класс
std::filesystem::path
имеет самое первостепенное значение в тех ситуациях, когда мы используем библиотеку, связанную с файловой системой, поскольку с ним связано большинство функций и классов.

Функция

filesystem::exists
позволяет проверить, существует ли указанный путь файла на самом деле. До сего момента мы не можем быть в этом уверены, поскольку возможно создавать объекты класса
path
, которые не относятся к реальному объекту файловой системы. Функция
exists
всего лишь принимает экземпляр класса
path
и возвращает значение true в том случае, если он действительно существует. Эта функция способна сама определить, какой путь мы ей передали (абсолютный или относительный), что делает ее очень комфортной в применении.

Наконец, мы использовали функцию

filesystem::canonical
для каталога, чтобы вывести на экран его нормализованную форму:


path canonical(const path& p, const path& base = current_path());


Функция

canonical
принимает путь к файлу и в качестве необязательного второго аргумента еще один путь к файлу. Второй путь
base
добавляется к пути файла
p
в том случае, если
p
является относительным. После этого функция
canonical
пытается убрать все косвенные адреса
.
и
..
.

Для вывода результата на экран мы использовали метод

.c_str()
, которому передали канонический путь к файлу. Мы сделали так потому, что перегруженный оператор
<<
для выходных потоков берет пути к файлам в кавычки, а это не всегда желательно.


Дополнительная информация

Функция

canonical
генерирует исключение типа
fileystem_error
, если путь, который мы хотим привести к каноническому виду, не существует. Для предотвращения этого мы проверили наш путь к файлу с помощью функции
exists
. Но было ли достаточно данной проверки, чтобы необработанные исключения не генерировались? Нет.

Обе функции, как

exists
, так и
canonical
, способны генерировать исключения типа
bad_alloc
. Если бы эти исключения сгенерировались, кто-то мог бы утверждать, что программа все равно обречена. Более важная, а также гораздо более вероятная проблема возникает, когда где-то между проверкой существования файла и процессом приведения его к каноническому виду некто переименовывает или удаляет основной файл! В этом случае функция
canonical
сгенерирует сообщение об ошибке
filesystem_error
, хотя мы ранее уже убедились в том, что файл существует.

Большая часть функций файловой системы имеет еще одну перегруженную версию, которая принимает те же аргументы, а также ссылку на

std::error_code
:


path canonical(const path& p, const path& base = current_path());

path canonical(const path& p, error_code& ec);

path canonical(const std::filesystem::path& p,

        const std::filesystem::path& base,

         std::error_code& ec);


Таким образом, можно выбрать, окружать ли запросы к функциям файловой системы конструктами

try-catch
, или же проверять наличие ошибок вручную. Обратите внимание: это изменяет только поведение ошибок, связанных с файловой системой! Если в системе заканчивается память, то возможна генерация более сложных исключений, таких как
bad_alloc
, которые могут иметь или не иметь параметр
ec
.

Получаем канонические пути к файлам из относительных путей

В последнем примере мы уже приводили к каноническому виду/нормализовали пути файлов. Конечно же, класс

filesystem::path
способен не только хранить и проверять пути к файлам. Это помогает легко создавать пути к файлу из строк, а также снова разбивать их на составные части.

На данном этапе класс path позволяет абстрагироваться от деталей работы операционной системы, но в некоторых случаях все же о них следует помнить.

Мы увидим, как обращаться с путями и их композицией/декомпозицией, на примере работы с абсолютными и относительными путями.


Как это делается

В данном примере мы будем работать с абсолютными и относительными путями к файлам, чтобы увидеть сильные стороны класса

path
и связанных с ним вспомогательных функций.


1. Сначала включаем все необходимые заголовочные файлы и объявляем, что используем пространства имен

std
и
filesystem
:


#include 

#include 


using namespace std;

using namespace filesystem;


2. Затем объявляем пример пути к файлу. На данный момент неважно, существует ли текстовый файл, на который он ссылается. Тем не менее есть функции, генерирующие исключения, если требуемого файла нет.


int main()

{

  path p {"testdir/foobar.txt"};


3. Сейчас мы познакомимся с четырьмя разными функциями библиотеки для работы с файловой системой. Функция

current_path
возвращает путь, в котором в данный момент выполняется программа, — так называемый рабочий каталог. Функция
absolute
принимает относительный путь к файлу наподобие нашего пути
p
и возвращает абсолютный, однозначный путь во всей файловой системе.

Функция

system_complete
делает практически то же самое, что и функция
absolute
в Linux, MacOS или других UNIX-подобных операционных системах. В Windows мы получим абсолютный путь к файлу, только вначале будет добавлено буквенное обозначение тома диска (например,
"C:"
). Функция
canonical
опять же делает то же самое, что и функция
absolute
, но потом дополнительно убирает все косвенные адреса, такие как "
.
" (сокращение для «текущий каталог») или "
..
" (сокращение для «один каталог вверх»). Мы рассмотрим работу с данными косвенными адресами в следующих шагах.


cout << "current_path    : " << current_path()

   << "\nabsolute_path  : " << absolute(p)

   << "\nsystem_complete : " << system_complete(p)

   << "\ncanonical(p)   : " << canonical(p)

   << '\n';


4. Еще одна приятная особенность класса

path
заключается в том, что он перегружает оператор
/
. Таким образом, можно сцеплять имена папок и имена файлов с помощью данного оператора и составлять из них пути файлов. Попробуем это и отобразим составной путь:


cout << path{"testdir"} / "foobar.txt" << '\n';


5. Рассмотрим работу с функцией canonical и составными путями. Передавая функции

canonical
относительный путь к файлу, например
"foobar.txt"
, и составной абсолютный путь
current_path()/"testdir"
, получаем существующий абсолютный путь к файлу. В следующем обращении к функции передаем наш путь
p
(т.е.
"testdir/foobar.txt"
) и абсолютный путь
current_path()
, который направляет нас в каталог
"testdir"
и обратно. Это то же самое, что и
current_path()
, из-за косвенных адресов. В обоих вызовах функция
canonical
должна возвратить одинаковый абсолютный путь.


cout << "canonical testdir : "

   << canonical("foobar.txt",

          current_path()/"testdir")

   << "\ncanonical testdir 2 : "

   << canonical(p, current_path()/"testdir/..")

   << '\n';


6. Кроме того, можно проверить эквивалентность двух путей, не являющихся каноническими. Функция

equivalence
приводит к каноническому виду пути к файлам, которые она принимает в качестве аргументов, и в конечном итоге возвращает значение
true
при условии, что они описывают один и тот же путь. Для этой проверки путь к файлу должен действительно существовать, в противном случае функция сгенерирует исключение.


  cout << "equivalence: "

    << equivalent("testdir/foobar.txt",

           "testdir/../testdir/foobar.txt")

    << '\n';

}


7. Компиляция и запуск программы дадут следующий результат. Функция

current_path()
возвращается к домашнему каталогу на моем ноутбуке, поскольку я запустил приложение оттуда. К нашему относительному пути
p
был добавлен префикс, состоящий из данной папки, с помощью функций
absolute_path
,
system_complete
и
canonical
. Мы видим, что функции
absolute_path
и
system_complete
выдают абсолютно одинаковое описание пути файла в моей системе, потому что это Mac (на Linux будет так же). В компьютере с операционной системой Windows функция
system_complete
добавит префикс
"C:"
или любого другого диска, в котором расположен рабочий каталог.


$ ./canonical_filepath

current_path   : "/Users/tfc"

absolute_path   : "/Users/tfc/testdir/foobar.txt"

system_complete : "/Users/tfc/testdir/foobar.txt"

canonical(p)   : "/Users/tfc/testdir/foobar.txt"

"testdir/foobar.txt"

canonical testdir  : "/Users/tfc/testdir/foobar.txt"

canonical testdir 2 : "/Users/tfc/testdir/foobar.txt"

equivalence: 1


8. Мы не обрабатываем никаких исключений в нашей короткой программе. При удалении файла

foobar.txt
из каталога
testdir
программа прекращает свою работу из-за исключения. Функция
canonical
требует наличия действительного пути файла. Существует также функция
weakly_canonical
, которая не предъявляет подобных требований.


$ ./canonial_filepath

current_path : "/Users/tfc"

absolute_path : "/Users/tfc/testdir/foobar.txt"

system_complete : "/Users/tfc/testdir/foobar.txt"

terminate called after throwing an instance of

'std::filesystem::v1:: cxx11::filesystem_error'

what(): filesystem error: cannot canonicalize:

No such file or directory [testdir/foobar.txt] [/Users/tfc]


Как это работает

Цель данного примера заключается в том, чтобы увидеть, как легко создавать новые пути динамически. В основном это связано с наличием в классе

path
удобного перегруженного оператора
/
. Кроме того, функции файловой системы хорошо работают с абсолютными и относительными путями к файлам, а также с путями, которые содержат косвенные адреса
.
и
..
.

Есть довольно много функций, которые возвращают части экземпляра

path
, иногда даже преобразуя их. Не будем перечислять все существующие функции, поскольку для знакомства с ними лучше всего обратиться к справочным материалам по C++.

Однако функции-члены класса

path
, возможно, стоит рассмотреть поближе. Посмотрим, каким функциям-членам класса
path
соответствуют конкретные части пути к файлу. На следующей диаграмме показано, что пути файлов в Windows несколько отличаются от путей файлов в UNIX/Linux (рис. 10.1).

Как видите, функции-члены класса

path
возвращаются для абсолютного пути. Для относительных путей
root_path
,
root_name
и
root_directory
пусты.
relative_ path
, соответственно, возвращает путь, только если тот уже является относительным.

Составляем список всех файлов в каталоге

Конечно же, каждая операционная система, предлагающая поддержку файловой системы, также поставляется с утилитой, которая просто перечисляет все файлы внутри каталога в файловой системе. Самые простые примеры — команда

ls
в Linux, MacOS и других UNIX-подобных операционных системах. В DOS и Windows существует команда
dir
. Обе команды составляют список из всех файлов в каталоге и предоставляют дополнительную информацию, такую как размер файла, разрешения и т.д.

Однако переопределение такого инструмента также является хорошим примером, который позволит нам научиться выполнять обходы каталогов и файлов. Давайте просто сделаем это!

Наша собственная утилита

ls/dir
будет способна упорядочивать по имени все файлы в каталоге, их флаги разрешения доступа и отображать количество байт, которые они занимают в файловой системе.


Как это делается

В этом примере мы реализуем небольшой инструмент, который составляет список файлов в любом предоставленном пользователем каталоге. Он будет упорядочивать файлы в списке не только по имени, но и по типу, размеру и разрешениям доступа.


1. Сначала включим необходимые заголовочные файлы и объявим об использовании пространств имен

std
и
filesystem
по умолчанию:


#include 

#include 

#include 

#include 

#include 

#include 

#include 


using namespace std;

using namespace filesystem;


2. Понадобится вспомогательная функция

file_info
. Она принимает ссылку на объект
directory_entry
и извлекает из нее путь, а также объект
file_status
(с помощью функции
status
), который содержит тип файла и информацию о правах. Наконец, она извлекает и размер записи, если это обычный файл. Для каталогов и особых файлов мы просто возвращаем значение
0
. Вся информация упаковывается в кортеж.


static tuple

file_info(const directory_entry &entry)

{

  const auto fs (status(entry));

 return {entry.path(),

      fs,

      is_regular_file(fs) ? file_size(entry.path()) : 0u};

}


3. Кроме того, понадобится вспомогательная функция

type_char
. Путь может представлять не только каталоги и простые текстовые/бинарные файлы. Операционные системы предоставляют множество разнообразных типов, которые абстрагируют что-то еще, например интерфейсы аппаратных устройств в виде так называемых символьных/блочных файлов. В библиотеке для работы с файловой системой, расположенной в STL, есть множество функций-предикатов для них. Таким образом, можно вернуть букву
'd'
для каталогов, букву
'f'
для обычных файлов и т.д.:


static char type_char(file_status fs)

{

  if  (is_directory(fs))      { return 'd'; }

  else if (is_symlink(fs))     { return 'l'; }

  else if (is_character_file(fs)) { return 'c'; }

  else if (is_block_file(fs))   { return 'b'; }

  else if (is_fifo(fs))       { return 'p'; }

  else if (is_socket(fs))     { return 's'; }

  else if (is_other(fs))      { return 'o'; }

  else if (is_regular_file(fs))  { return 'f'; }

  return '?';

}


4. Напишем еще одну вспомогательную функцию,

rwx
. Она принимает переменную
perms
(просто возвращающую тип класса перечисления из библиотеки для работы с файловой системой) и возвращает строку наподобие
"rwxrwxrwx"
, которая описывает настройки прав для файла. Первая группа символов
"rwx"
описывает права на чтение, запись и исполнение (read, write and execution) для владельца файла. Следующая группа описывает те же права для всех пользователей, являющихся частью пользовательской группы, к которой принадлежит файл. Последняя группа символов описывает эти же права для всех остальных. Строка
"rwxrwxrwx"
означает, что все пользователи могут получить доступ к объекту любым способом; строка
"rw-r--r--"
— что только владелец файла может читать и изменять его, а все остальные — только читать. Мы просто создадим строку на основе этих значений бит за битом. Лямбда-выражение поможет выполнить повторяющуюся работу, связанную с проверкой, содержит ли переменная
p
типа
perms
конкретный бит владельца, и возвратит символ
'-'
или соответствующую букву:


static string rwx(perms p)

{

  auto check ([p](perms bit, char c) {

   return (p & bit) == perms::none ? '-' : c;

  });

  return {check(perms::owner_read,   'r'),

      check(perms::owner_write,  'w'),

       check(perms::owner_exec,   'x'),

      check(perms::group_read,   'r'),

      check(perms::group_write,  'w'),

       check(perms::group_exec,   'x'),

      check(perms::others_read,  'r'),

      check(perms::others_write, 'w'),

      check(perms::others_exec,  'x')};

}


5. Наконец, последняя вспомогательная функция принимает целочисленный размер файла и преобразует его в читабельный вид. Мы просто проигнорируем точку при делении чисел и округлим их до ближайшего значения кило-, мега- или гигабайт.


static string size_string(size_t size)

{

  stringstream ss;

  if (size >= 1000000000) {

   ss <<  (size / 1000000000) << 'G';

  } else if (size >= 1000000) {

   ss <<  (size / 1000000) << 'M';

  } else if (size >= 1000) {

   ss <<  (size / 1000) << 'K';

  } else { ss << size << 'B'; }

  return ss.str();

}


6. Теперь наконец можно реализовать функцию

main
. Начнем с проверки того, предоставил ли пользователь путь в командной строке. Если он этого не сделал, то просто возьмем текущий каталог
"."
. Затем проверим, существует ли данный каталог. При его отсутствии мы не можем создать список файлов.


int main(int argc, char *argv[])

{

  path dir {argc > 1 ? argv[1] : "."};

 if (!exists(dir)) {

   cout << "Path " << dir << " does not exist.\n";

   return 1;

  }


7. Заполним вектор кортежами, содержащими информацию о файлах, которые возвращает наша первая вспомогательная функция

file_info
из объектов
directory_entry
. Создадим
directory_iterator
и передадим в его конструктор объект пути, созданный на предыдущем шаге. При переборе с помощью итератора для каталогов преобразуем объекты типа
directory_entry
в кортежи, содержащие информацию о файлах, и вставляем их в вектор:


  vector> items;

  transform(directory_iterator{dir}, {},

  back_inserter(items), file_info);


8. Мы сохранили всю необходимую информацию в элементы вектора и можем просто сохранить ее с помощью написанных вспомогательных функций:


  for (const auto &[path, status, size] : items) {

   cout << type_char(status)

     << rwx(status.permissions()) << " "

     << setw(4) << right << size_string(size)

     << " " << path.filename().c_str()

     << '\n';

  }

}


9. Компиляция и запуск проекта с путем к офлайн-версии документации C++ дадут следующий результат. Мы видим, что папка содержит только другие каталоги и простые файлы, поскольку первыми символами каждой строки являются

'd'
и
'f'
. Данные файлы имеют разные права доступа и, конечно, различаются по размеру. Обратите внимание: файлы представлены в алфавитном порядке, но мы не можем полагаться на эту особенность, поскольку это не требуется в стандарте С++17.


$ ./list ~/Documents/cpp_reference/en/cpp

drwxrwxr-x   0B algorithm

frw-r--r--  88K algorithm.html

drwxrwxr-x   0B atomic

frw-r--r--  35K atomic.html

drwxrwxr-x   0B chrono

frw-r--r--  34K chrono.html

frw-r--r--  21K comment.html

frw-r--r--  21K comments.html

frw-r--r-- 220K compiler_support.html

drwxrwxr-x   0B concept

frw-r--r--  67K concept.html

drwxr-xr-x  0B container

frw-r--r-- 285K container.html

drwxrwxr-x  0B error

frw-r--r--  52K error.html


Как это работает

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

Чтобы обойти каталог, мы просто создали итератор

directory_iterator
и проитерировали с его помощью. Обход каталога фантастически легко совершить с помощью библиотеки
filesystem
:


for (const directory_entry &e : directory_iterator{dir}) {

  // сделать что-то

}


Про этот класс можно сказать лишь следующее:

□ он проверяет каждый элемент один раз;

□ порядок, в котором выполняется перебор, не определен;

□ элементы каталога

.
и
..
уже отфильтрованы.


Однако можно заметить, что итератор

directory_iterator
ведет себя как итератор и как итерабельный диапазон одновременно. Почему? В небольшом примере с циклом
for
мы видели, как он используется в качестве итерабельного диапазона. В самом коде примера мы применили его как итератор:


transform(directory_iterator{dir}, {},

      back_inserter(items), file_info);


Правда заключается в том, что это всего лишь класс итератора, но функции

std::begin
и
std::end
предоставляют перегруженные версии данного типа. Данное обстоятельство позволяет вызвать функции
begin
и
end
для подобного итератора, и они снова будут возвращать итераторы. Это может показаться странным на первый взгляд, но делает наш класс более полезным.

Инструмент текстового поиска в стиле grep

Большая часть операционных систем оснащена неким локальным инструментом поиска. Пользователи могут запустить его нажатием сочетания клавиш, а затем ввести имя локального файла, который хотели бы найти.

Прежде чем появилась подобная возможность, пользователи командной строки выполняли поиск файлов с помощью таких инструментов, как

grep
или
awk
. Пользователь мог просто ввести команду наподобие
"grep -r foobar ."
, и инструмент рекурсивно прошел бы по текущему каталогу и нашел бы все файлы, содержащие строку
"foobar"
.

В этом примере мы реализуем точно такое же приложение. Наш небольшой клон

grep
будет принимать шаблон из командной строки, а затем выполнять рекурсивный поиск в том каталоге, где мы находимся на момент запуска приложения. Он выведет имена всех файлов, соответствующих нашему шаблону. Проверка на совпадение с шаблоном будет применяться построчно, так что можно вывести на экран еще и номера строк в файле, соответствующих шаблону.


Как это делается

В этом разделе мы реализуем небольшой инструмент, который выполняет поиск предоставленных пользователем текстовых шаблонов в файлах. Инструмент работает точно так же, как и инструмент UNIX

grep
, но для простоты будет не таким зрелым и эффективным.


1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространств имен

std
и
filesystem
:


#include 

#include 

#include 

#include 

#include 

#include 


using namespace std;

using namespace filesystem;


2. Потом реализуем вспомогательную функцию. Она принимает путь к файлу и объект, содержащий регулярное выражение, описывающее искомый шаблон. Затем создаем экземпляр вектора, который будет включать пары, состоящие из номера строк и их содержимого. Кроме того, создадим экземпляр объекта файлового потока ввода, из которого будем считывать содержимое и сравнивать его с шаблоном строка за строкой:


static vector>

matches(const path &p, const regex &re)

{

  vector> d;

 ifstream is {p.c_str()};


3. Пройдем по файлу строка за строкой с помощью функции

getline
. Функция
regex_search
возвращает значение
true
при условии, что строка содержит наш шаблон. Если это именно так, то поместим в вектор номер строки и саму строку. Наконец, вернем все найденные совпадения:


  string s;

  for (size_t line {1}; getline(is, s); ++line) {

   if (regex_search(begin(s), end(s), re)) {

    d.emplace_back(line, move(s));

    }

  }

  return d;

}


4. В функции

main
сначала проверим, предоставил ли пользователь аргумент командной строки, который можно задействовать как шаблон. Если нет, то сгенерируем ошибку:


int main(int argc, char *argv[])

{

  if (argc != 2) {

   cout << "Usage: " << argv[0] << " \n";

   return 1;

}


5. Далее создадим объект регулярного выражения из входного шаблона. Невозможность создать такое регулярное выражение приведет к генерации исключения. При генерации исключения поймаем его и сгенерируем ошибку:


  regex pattern;

  try { pattern = regex{argv[1]}; }

 catch (const regex_error &e) {

   cout << "Invalid regular expression provided.n";

   return 1;

  }


6. Наконец, можно проитерировать по файловой системе и поискать совпадения с шаблоном. Воспользуемся итератором

recursive_directory_iterator
для итерации по всем файлам рабочего каталога. Он работает точно так же, как и итератор
directory_iterator
из предыдущего примера, но заходит еще и в подкаталоги. Таким образом, не нужно управлять рекурсией. Для каждой записи вызываем вспомогательную функцию matches:


  for (const auto &entry :

      recursive_directory_iterator{current_path()}) {

    auto ms (matches(entry.path(), pattern));


7. Для каждого совпадения (если они есть) выводим путь к файлу, номер строки и содержимое строки, содержащей совпадение:


   for (const auto &[number, content] : ms) {

    cout << entry.path().c_str() << ":" << number

      << " - " << content << '\n';

   }

  }

}


8. Подготовим файл с именем

"foobar.txt"
, содержащий тестовые строки, по которым можно выполнить поиск:


foo

bar

baz


9. Компиляция и запуск программы дадут следующий результат. Я запустил приложение в каталоге

/Users/tfc/testdir
моего ноутбука и сначала передал ему шаблон
"bar"
. Внутри этого каталога приложение нашло вторую строку в нашем файле
"foobar.txt"
и другом файле
"text1.txt"
, который находится в каталоге
testdir/dir1
:


$ ./grepper bar

/Users/tfc/testdir/dir1/text1.txt:1 - foo bar bla blubb

/Users/tfc/testdir/foobar.txt:2 - bar


10. При повторном запуске приложения с шаблоном

"baz"
оно находит третью строку в нашем примере текстового файла:


$ ./grepper baz

/Users/tfc/testdir/foobar.txt:3 - baz


Как это работает

Создание и использование регулярного выражения с целью фильтрации содержимого файлов — основная цель данного примера. Однако рассмотрим итератор

recursive_directory_iterator
, поскольку этот особый класс итераторов мы применяли для фильтрации файлов, по которым итерируем рекурсивно.

Как и

directory_iterator
,
recursive_directory_iterator
итерирует по элементам каталога. Он делает это рекурсивно, согласно своему названию. При встрече с элементом файловой системы, который является каталогом, он вернет экземпляр типа
directory_entry
для данного пути, а затем зайдет в него, чтобы проитерировать по его потомкам.

Итератор

recursive_directory_iterator
имеет несколько интересных функций-членов.

depth()
— говорит, на сколько уровней итератор спустился в подкаталоге.

recursion_pending()
— сообщает, будет ли итератор спускаться дальше после элемента, на который он указывает в данный момент.

disable_recursion_pending()
— эту функцию можно вызвать, чтобы помешать итератору спуститься в следующий подкаталог, если сейчас он указывает на каталог, в который можно спуститься. Это значит, что вызов указанного метода ничего не даст, если мы совершим данное действие слишком рано.

pop()
— эта функция прерывает работу на текущем уровне и поднимает итератор на один уровень вверх в иерархии каталогов для продолжения работы.


Дополнительная информация

Еще одной важной деталью, о которой нужно знать, выступает класс-перечисление

directory_options
. Конструктор класса
recursive_directory_iterator
принимает значение этого типа в качестве второго аргумента. Значением по умолчанию, которое мы использовали неявно, является
directory_options::none
. Другие его значения выглядят следующим образом:

follow_directory_symlink
— позволяет рекурсивному итератору следовать по символьным ссылкам на каталоги;

skip_permission_denied
— указывает итератору пропускать каталоги, которые в противном случае вернут ошибку, поскольку файловая система не дает прав на доступ к ним.


Эти настройки можно объединять с помощью оператора

|
.

Инструмент для автоматического переименования файлов

На создание этого примера меня сподвигла ситуация, в которую я попадаю довольно часто. Скажем, при объединении в одном каталоге файлов с фотографиями от разных друзей и с разных устройств можно заметить, что расширения этих файлов различаются. Одни файлы формата JPEG имеют расширение

.jpg
, другие —
.jpeg
, а третьи — и вовсе
.JPEG
.

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

' '
и заменить их, например, на
'_'
.

В данном примере мы реализуем такой инструмент и назовем его

renamer
. Он будет принимать диапазон входных шаблонов и их замен, это выглядит следующим образом:


$ renamer jpeg jpg JPEG jpg


В этом случае

renamer
рекурсивно проитерирует по текущему каталогу и выполнит поиск шаблонов
jpeg
и
JPEG
в именах всех файлов. Он заменит обе строки на
jpg
.


Как это делается

В этом примере мы реализуем инструмент, который рекурсивно просканирует все файлы внутри каталога и соотнесет их имена с заданным шаблоном. Все совпадения заменятся токенами, предоставленными пользователем, и найденные файлы будут переименованы соответствующим образом.


1. Сначала включим некоторые заголовочные файлы и объявим об использовании пространств имен

std
и
filesystem
:


#include 

#include 

#include 

#include 


using namespace std;

using namespace filesystem;


2. Реализуем небольшую вспомогательную функцию, которая принимает путь к файлу в виде строки, а также диапазон пар для замены. Каждая пара для замены содержит шаблон и его замену. Проходя в цикле по диапазону, воспользуемся

regex_replace
, передавая ему входную строку и принимая преобразованную строку. После этого вернем полученную строку:


template 

static string replace(string s, const T &replacements)

{

  for (const auto &[pattern, repl] : replacements) {

   s = regex_replace(s, pattern, repl);

  }

  return s;

}


3. В функции

main
сначала проверим командную строку. Принимаем аргументы из нее попарно, поскольку нужно, чтобы шаблоны стояли рядом с их заменами. Первый элемент
argv
— всегда имя исполняемого файла. Это значит, что если пользователь предоставит хотя бы одну пару или больше, то
argc
должен быть нечетным и не меньше
3
:


int main(int argc, char *argv[])

{

  if (argc < 3 || argc % 2 != 1) {

   cout << "Usage: " << argv[0]

     << "   ...\n";

   return 1;

  }


4. Как только мы убедились, что получили во входных значениях необходимые пары, заполним ими вектор:


  vector> patterns;

 for (int i {1}; i < argc; i += 2) {

   patterns.emplace_back(argv[i], argv[i + 1]);

}


5. Теперь можно проитерировать по файловой системе. Для простоты определим в качестве каталога, по которому нужно проитерировать, текущий каталог приложения. Затем возьмем только имена файлов без остальной части пути и преобразуем их соответственно списку шаблонов и замен, собранному ранее. Возьмем копию

opath
, назовем ее
rpath
и заменим часть имени файла новой:


for (const auto &entry :

   recursive_directory_iterator{current_path()}) {

  path opath {entry.path()};

  string rname {replace(opath.filename().string(),

         patterns)};

  path rpath {opath};

 rpath.replace_filename(rname);


6. Для всех файлов, чьи имена совпали с нашими шаблонами, выведем на экран строку, сигнализирующую о том, что мы переименовали их. Если полученное в результате переименования имя файла уже существует, то продолжать работу невозможно. Просто будем пропускать такие файлы. Вместо этого мы могли бы прикреплять какое-то число к пути или что-то еще с целью разрешить пересечение имен.


   if (opath != rpath) {

    cout << opath.c_str() << " --> "

       << rpath.filename().c_str() << '\n';

    if (exists(rpath)) {

     cout << "Error: Can't rename."

         " Destination file exists.\n";

    } else {

     rename(opath, rpath);

    }

   }

  }

}


7. Компиляция и запуск программы для примера каталога дадут следующий результат. Я поместил несколько картинок в формате JPEG в каталог, но задал для них разные окончания:

jpg
,
jpeg
и
JPEG
. Затем выполнил программу, передав ей шаблоны
jpeg
и
JPEG
и выбрав замену
jpg
для них обоих. В результате получил каталог с одинаковыми расширениями файлов:


$ ls

birthday_party.jpeg holiday_in_dubai.jpg holiday_in_spain.jpg

trip_to_new_york.JPEG

$ ../renamer jpeg jpg JPEG jpg

/Users/tfc/pictures/birthday_party.jpeg --> birthday_party.jpg

/Users/tfc/pictures/trip_to_new_york.JPEG --> trip_to_new_york.jpg

$ ls

birthday_party.jpg holiday_in_dubai.jpg holiday_in_spain.jpg

trip_to_new_york.jpg

Создаем индикатор эксплуатации диска

Мы уже реализовали инструмент, который работает как

ls
в Linux/MacOS или dir в Windows, но, подобно этим утилитам, не выводит размер файлов в каталогах.

Чтобы получить эквивалент размера каталога, нужно зайти во все подкаталоги и суммировать размеры всех файлов, содержащихся в них.

В данном примере мы реализуем инструмент, который делает именно это. Инструмент можно запустить для любого каталога, он определит размер всех его записей.


Как это делается

В этом примере мы реализуем приложение, которое итерирует по каталогу и перечисляет размеры файлов для каждой записи. Это просто для обычных файлов, но если мы смотрим на запись каталога, которая сама по себе является каталогом, то нужно заглянуть в него и суммировать размеры всех файлов, хранящихся в нем.


1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространств имен

std
и
filesystem
:


#include 

#include 

#include 

#include 

#include 


using namespace std;

using namespace filesystem;


2. Затем реализуем вспомогательную функцию, которая принимает в качестве аргумента

directory_entry
и возвращает его размер в файловой системе. Это не каталог, мы просто вернем размер файла, вычисленный с помощью
file_size
:


static size_t entry_size(const directory_entry &entry)

{

  if (!is_directory(entry)) { return file_size(entry); }


3. Если это каталог, то нужно проитерировать по всем его записям и подсчитать их размер. Мы будем вызывать вспомогательную функцию

entry_size
рекурсивно при повторной встрече с подкаталогами:


  return accumulate(directory_iterator{entry}, {}, 0u,

     [](size_t accum, const directory_entry &e) {

   return accum + entry_size(e);

  });

}


4. Для повышения читабельности воспользуемся функцией

size_string
, которая уже встречалась в этой главе. Она просто сокращает большие размеры файлов, делая их «аккуратнее» и добавляя префиксы «кило», «мега» или «гига»:


static string size_string(size_t size)

{

  stringstream ss;

  if     (size >= 1000000000) {

   ss <<  (size / 1000000000) << 'G';

  } else if (size >= 1000000) {

   ss <<  (size / 1000000) << 'M';

  } else if (size >= 1000) {

   ss <<  (size / 1000) << 'K';

  } else { ss << size << 'B'; }

  return ss.str();

}


5. Первое, что нужно сделать в функции

main
, — проверить, предоставил ли пользователь путь к файлу в командной строке. Если это не так, то возьмем текущий каталог. Прежде чем продолжить, проверим, существует ли данный каталог:


int main(int argc, char *argv[])

{

  path dir {argc > 1 ? argv[1] : "."};

 if (!exists(dir)) {

   cout << "Path " << dir << " does not exist.\n";

   return 1;

}


6. Теперь можно проитерировать по всем записям каталога и вывести на экран их имена и размер:


  for (const auto &entry : directory_iterator{dir}) {

   cout << setw(5) << right

     << size_string(entry_size(entry))

     << " " << entry.path().filename().c_str()

     << '\n';

  }

}


7. Компиляция и запуск программы дадут следующий результат. Я запустил ее для каталога, в котором находится офлайн-справка по С++. Поскольку он содержит и подкаталоги, наша вспомогательная функция, суммирующая размер файла, очень пригодится:


$ ./file_size ~/Documents/cpp_reference/en/

 19M c

 12K c.html

147M cpp

 17K cpp.html 22K index.html

 22K Main_Page.html


Как это работает

Вся программа строится на использовании функции

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

Единственное, что мы сделали для определения того, можем ли вызвать непосредственно

file_size
или нужно рекурсивно спуститься дальше, — реализовали предикат
is_directory
. Он работает для каталогов, которые содержат только обычные файлы и каталоги.

Поскольку наша программа довольно проста, она даст сбой при следующих условиях.

□ Функция

file_size
работает только для обычных файлов и символьных ссылок. Она генерирует исключение во всех других случаях.

□ Несмотря на то что функция

file_size
работает для символьных ссылок, она все еще сгенерирует исключение, если мы вызовем ее для неработающей символьной ссылки.


Чтобы сделать программу из нашего примера более надежной, следует воспользоваться защитным программированием в случаях неверных типов файлов и обработки исключений.

Подбиваем статистику о типах файлов

В предыдущем примере мы реализовали инструмент, который выводит на экран размер всех членов каталога.

В текущем примере тоже будем определять размер рекурсивно, но в этот раз объединим размеры всех файлов с одинаковым расширением. Таким образом, сможем вывести для пользователя таблицу, в которой перечисляется количество и тип файлов, а также средний размер файлов каждого типа.


Как это делается

В этом примере мы реализуем небольшой инструмент, который рекурсивно итерирует по заданному каталогу. В процессе он определяет, файлы с какими расширениями находятся в данном каталоге, сколько файлов присутствует для каждого расширения, а также их средний размер.


1. Сначала включим необходимые заголовочные файлы и объявим об использовании пространств имен

std
и
filesystem
:


#include 

#include 

#include 

#include 

#include 


using namespace std;

using namespace filesystem;


2. Функция

size_string
была полезна в предыдущих примерах. Она преобразует размеры файлов в читабельные строки:


static string size_string(size_t size)

{

  stringstream ss;

  if (size >= 1000000000) {

   ss <<  (size / 1000000000) << 'G';

  } else if (size >= 1000000) {

   ss <<  (size / 1000000) << 'M';

  } else if (size >= 1000) {

   ss <<  (size / 1000) << 'K';

  } else { ss << size << 'B'; }

return ss.str();

}


3. Затем реализуем вспомогательную функцию, которая принимает объект пути и итерирует по всем файлам данного пути. При этом она собирает всю информацию в ассоциативный массив, в котором соотносятся расширения файлов и пары, содержащие общее количество файлов и их суммарный размер.


static map> ext_stats(const path &dir)

{

  map> m;

 for (const auto &entry :

    recursive_directory_iterator{dir}) {


4. Если запись каталога тоже является каталогом, то ее можно опустить. Это не значит, что мы не будем рекурсивно спускаться в каталог. Итератор

recursive_ directory_iterator
все равно совершит данное действие, но мы не хотим сами заходить в эти подкаталоги.


   const path p {entry.path()};

   const file_status fs {status(p)};

   if (is_directory(fs)) { continue; }


5. Далее извлекаем расширения из строки, представляющей запись каталога. Если у нее нет расширения, то просто опускаем ее:


  const string ext {p.extension().string()};

  if (ext.length() == 0) { continue; }


6. Подсчитаем размер текущего файла. Затем найдем агрегатный объект в ассоциативном массиве для этого расширения. Если такого объекта нет, то неявно создадим его. Просто увеличим счетчик количества файлов и добавим размер файла в переменную-аккумулятор:


   const size_t size {file_size(p)};


   auto &[size_accum, count] = m[ext];

   size_accum += size;

   count    += 1;

  }


7. После этого вернем ассоциативный массив:


  return m;

}


8. В функции

main
примем путь, предоставленный пользователем в командной строке. Конечно, нужно проверить, существует ли он, поскольку в противном случае продолжать не имеет смысла.


int main(int argc, char *argv[])

{

  path dir {argc > 1 ? argv[1] : "."};

 if (!exists(dir)) {

   cout << "Path " << dir << " does not exist.\n";

   return 1;

  }


9. Можно мгновенно проитерировать по ассоциативному массиву, предоставляемому

ext_stats
. Поскольку элементы типа
accum_size
этого массива содержат сумму всех файлов с одинаковым расширением, разделим эту сумму на общее количество таких файлов и выведем ее на экран:


  for (const auto &[ext, stats] : ext_stats(dir)) {

   const auto &[accum_size, count] = stats;

   cout << setw(15) << left << ext << ": "

     << setw(4) << right << count

     << " items, avg size "

     << setw(4) << size_string(accum_size / count)

     << '\n';

  }

}


10. Компиляция и запуск программы дадут следующий результат. Я предоставил ей в качестве аргумента командной строки каталог, содержащий офлайн-справку по С++.


$ ./file_type ~/Documents/cpp_reference/

.css  :   2 items, avg size  41K

.gif  :   7 items, avg size 902B

.html  : 4355 items, avg size  38K

.js   :   3 items, avg size  4K

.php   :   1 items, avg size 739B

.png   :  34 items, avg size  2K

.svg   :  53 items, avg size  6K

.ttf   :   2 items, avg size 421K

Инструмент для уменьшения размера папки путем замены дубликатов символьными ссылками

Существует множество инструментов, сжимающих данные разными способами. Наиболее известными примерами таких алгоритмов/форматов являются ZIP и RAR. Подобные инструменты уменьшают размер файлов, снижая их внутреннюю избыточность.

Прежде чем сжать файлы в архив, можно довольно просто снизить использование диска, просто удалив повторяющиеся файлы. В данном примере мы реализуем небольшой инструмент, который рекурсивно проходит по каталогу и в процессе ищет файлы с одинаковым содержимым. Если он найдет такие файлы, то удалит все дубликаты, кроме одного. Все удаленные файлы будут заменены символьными ссылками, которые указывают на оставшийся уникальный файл. Это позволяет сэкономить место, не выполняя сжатия и сохраняя данные.


Как это делается

В данном примере мы реализуем небольшой инструмент, который определяет, какие файлы в каталоге дублируют друг друга. Зная это, он удалит все файлы, кроме одного, и заменит их символьными ссылками, что уменьшит размер каталога.


Убедитесь, что создали резервные копии системных данных. Мы будем работать с функциями STL, которые удаляют файлы. Неверно указанный путь для такой программы может привести к тому, что программа жадно удалит нежелательным способом слишком много файлов.


1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространств имен

std
и
filesystem
по умолчанию:


#include 

#include 

#include 

#include 


using namespace std;

using namespace filesystem;


2. Чтобы определить, какие файлы являются дубликатами друг друга, создадим ассоциативный массив, в котором соотносятся хеши файлов и путь к первому файлу, из которого был получен этот хеш. Для получения таких хешей следует использовать популярный алгоритм, такой как MD5 или SHA. В целях сохранения данного примера чистым и простым просто считаем весь файл в строку, а затем задействуем объект хеш-функции, уже применяемый

unordered_map
для подсчета хешей строк:


static size_t hash_from_path(const path &p)

{

  ifstream is {p.c_str(),

        ios::in | ios::binary};

  if (!is) { throw errno; }

  string s;

  is.seekg(0, ios::end);

  s.reserve(is.tellg());

  is.seekg(0, ios::beg);

 s.assign(istreambuf_iterator{is}, {});

 return hash{}(s);

}


3. Затем реализуем функцию, которая создает такой ассоциативный массив, основанный на хешах, и удаляет дубликаты. Она рекурсивно итерирует по каталогу и его подкаталогам:


static size_t reduce_dupes(const path &dir)

{

  unordered_map m; size_t count {0};

  for (const auto &entry :

   recursive_directory_iterator{dir}) {


4. Для каждой записи каталога функция проверяет, является ли эта запись каталогом. Все каталоги опускаются. Для каждого файла генерируем значение хеша и пробуем вставить его в ассоциативный массив. Если последний уже содержит такой хеш, то это значит, что файл с таким хешем уже был добавлен. Соответственно, мы нашли дубликат! В случае конфликтов во время вставки второе значение в паре, которую возвращает

try_emplace
, равно
false
.


   const path p {entry.path()};

   if (is_directory(p)) { continue; }

   const auto &[it, success] =

    m.try_emplace(hash_from_path(p), p);


5. Задействуя значения, возвращаемые

try_emplace
, мы можем сказать пользователю, что мгновение назад вставили файл, поскольку встретили этот хеш в первый раз. Если мы нашли дубликат, то сообщаем пользователю о том, что второй файл является дубликатом, и удаляем его. После удаления создаем символьную ссылку, которая заменяет дубликат.


   if (!success) {

    cout << "Removed " << p.c_str()

      << " because it is a duplicate of "

      << it->second.c_str() << '\n';

    remove(p);

    create_symlink(absolute(it->second), p);

    ++count;

   }


6. После перебора в файловой системе возвращаем количество файлов, которые мы удалили и заменили файловыми ссылками.


  }

  return count;

}


7. В функции

main
убеждаемся, что пользователь передал каталог в командной строке и что этот каталог существует:


int main(int argc, char *argv[])

{

  if (argc != 2) {

   cout << "Usage: " << argv[0] << " \n";

   return 1;

  }

  path dir {argv[1]};

  if (!exists(dir)) {

   cout << "Path " << dir << " does not exist.\n";

   return 1;

  }


8. Единственное, что нам осталось сделать, — вызвать функцию

reduce_dupes
для этого каталога и вывести на экран информацию о том, сколько файлов мы удалили:


  const size_t dupes {reduce_dupes(dir)};

  cout << "Removed " << dupes << " duplicates.\n";

}


9. Компиляция и запуск программу, для примера каталога, содержащего дубликаты, выглядит следующим образом. Я использовал инструмент

du
, чтобы проверить размер каталога до и после запуска нашей программы, с целью продемонстрировать работоспособность нашего подхода:


$ du -sh dupe_dir

1.1M dupe_dir

$ ./dupe_compress dupe_dir

Removed dupe_dir/dir2/bar.jpg because it is a duplicate of

dupe_dir/dir1/bar.jpg

Removed dupe_dir/dir2/base10.png because it is a duplicate of

dupe_dir/dir1/base10.png

Removed dupe_dir/dir2/baz.jpeg because it is a duplicate of

dupe_dir/dir1/baz.jpeg

Removed dupe_dir/dir2/feed_fish.jpg because it is a duplicate of

dupe_dir/dir1/feed_fish.jpg

Removed dupe_dir/dir2/foo.jpg because it is a duplicate of

dupe_dir/dir1/foo.jpg

Removed dupe_dir/dir2/fox.jpg because it is a duplicate of

dupe_dir/dir1/fox.jpg

Removed 6 duplicates.

$ du -sh dupe_dir

584K dupe_dir


Как это работает

Мы использовали функцию

create_symlink
, чтобы создать входную точку в другой файл в файловой системе. Это позволит избежать наличия дубликатов. Кроме того, можно создать жесткую ссылку с помощью функции
create_hard_link
. Семантически оба этих подхода похожи друг на друга, но жесткие ссылки имеют другие технические последствия, нежели мягкие. Некоторые форматы файловых систем и вовсе могут не поддерживать жесткие ссылки или, например, лишь определенное количество жестких ссылок, ссылающихся на один и тот же файл. Еще одна проблема заключается в том, что жесткие ссылки не могут указывать из одной файловой системы на другую.

Однако помимо деталей реализации существует еще один источник ошибок, проявляющийся при использовании

create_symlink
или
create_hard_link
. В следующих строках содержится ошибка. Можете ли вы ее заметить сразу?


path a {"some_dir/some_file.txt"};

path b {"other_dir/other_file.txt"};

remove(b);

create_symlink(a, b);


При выполнении этой программы ничего плохого не случится, но символьная ссылка будет нерабочей. Она указывает на

"some_dir/some_file.txt"
, а это неверно. Проблема заключается в том, что она должна указывать либо на
"/absolute/path/some_dir/some_file.txt"
, либо на
"../some_dir/some_file.txt"
. Вызов
create_ symlink
использует корректный абсолютный путь, если мы напишем следующий код:


create_symlink(absolute(a), b);


Функция

create_symlink
не проверяет, корректен ли путь, на который мы создаем ссылку.


Дополнительная информация

Как можно заметить, наша функция определения хеша довольно незамысловата. Мы реализовали ее такой, чтобы пример оставался простым и не имел внешних зависимостей.

В чем заключается проблема нашей хеш-функции? На самом деле есть даже две проблемы.

1. Мы считываем в строку весь файл. Это будет иметь катастрофические последствия для файлов, которые крупнее нашей системной памяти.

2. Типаж хеш-функции

hash
, представленный в С++, скорее всего, не поддерживает такие хеши.


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

Существуют более качественные алгоритмы хеширования, например MD5 или одна из разновидностей SHA. Чтобы получить доступ к таким функциям в нашей программе, можно использовать, скажем, криптографический API для OpenSSL.

Загрузка...