Страницы

Поиск по вопросам

воскресенье, 8 декабря 2019 г.

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

#linux #файловая_система


Сама задача следующая: программа получает на вход в аргументах командной строки имя
каталога и печатает на стандартный поток вывода суммарный размер (в байтах) всех регулярных
файлов в нем. При этом пропускать все записи, являющиеся символическими ссылками.

У меня есть написанная программа, которая успешно выдает какие-то числа, но я не
знаю, как убедиться в том, что она выдает правильный ответ. Подскажите, пожалуйста:

1) Можно ли как-то решить исходную задачу средствами bash (команда ls и так далее)? 

2) Какие могут быть "крайние" случаи, на которых программу стоит протестировать?
Ну кроме пустого каталога.
    


Ответы

Ответ 1



Чтобы подсчитать суммарный размер обычных файлов (S_ISREG) в заданном дереве директорий, пропуская все symlinks, на Питоне: #!/usr/bin/env python3 import os from contextlib import suppress def get_tree_size_scandir(path): """Return total size of all regular files in directory tree at *path*.""" size = 0 for entry in os.scandir(path): with suppress(OSError): # ignore errors for entry & its children if entry.is_dir(follow_symlinks=False): # directory size += get_tree_size_scandir(entry) elif entry.is_file(follow_symlinks=False): # regular file size += entry.stat(follow_symlinks=False).st_size return size if __name__ == "__main__": import sys print(get_tree_size_scandir(sys.argv[1])) Пример: $ ./get-tree-size /usr 7217750930 Вывод показывает, что общий размер всех обычных файлов в /usr директории, около 7 GB. Python, listdir() Для проверки, я реализовал get_tree_size(), не используя os.scandir(): #!/usr/bin/env python3 import os import stat from contextlib import suppress _dir_flags = os.O_RDONLY def get_tree_size_listdir_fd(fd): """Return total size of all regular files in directory tree at *fd*.""" size = 0 for name in os.listdir(fd): with suppress(OSError): # ignore errors for entry & its children st = os.lstat(name, dir_fd=fd) # don't follow symlinks if stat.S_ISDIR(st.st_mode): # directory top_fd = os.open(name, _dir_flags, dir_fd=fd) try: size += get_tree_size_listdir_fd(top_fd) finally: os.close(top_fd) elif stat.S_ISREG(st.st_mode): # regular file size += st.st_size return size if __name__ == "__main__": import sys print(get_tree_size_listdir_fd(os.open(sys.argv[1], _dir_flags))) Результаты одинаковые в данном случае, но в общем случае они могут отличаться (например, os.listdir() возвращает список (сразу все имена), а os.scandir() возвращает итератор, поэтому os.scandir() может учесть больше имен, а os.listdir() пропустит всю директорию, если произойдёт ошибка c получением хотя бы одного имени в директории). Код для примеров адаптирован из Python issue: PEP 471 implementation: os.scandir() directory scanning function. Внимание: размер файла и занимаемое место на диске могут отличаться. Bash, du 1) Можно ли как-то решить исходную задачу средствами bash (команда ls и так далее)? Можно, конечно, но результаты могут немного отличаться (см. тестовые случаи). Если нужны точные результаты, то несложно написать программу, с точным необходимым поведением как показывают примеры кода на Питоне выше. du -bs . возвращает значение, которое превышает суммарные размеры файлов, например: $ ls -l total 8 -rw-rw-r-- 1 me me 820 Oct 25 22:59 get_tree_size_fd.py -rw-rw-r-- 1 me me 631 Oct 25 23:07 get_tree_size_scandir.py Суммарный размер: 820 + 631 == 1451: $ python3 get_tree_size_fd.py . 1451 что ожидаемо (Питон возвращает правильный результат), но du возвращает неверный результат: $ du -bs . 5547 . -b опция уже включает в себя --apparent-size (то есть результат уже не отражает занимаемое место на диске -- как и хотелось). @avp упомянул: du считает также размеры всех каталогов, которые они занимают на диске. Что подтверждается экспериментами: $ mkdir dir # создаём пустую директорию $ python3 get_tree_size_fd.py . # результат ожидаемо не изменился 1451 $ du -bs . 9643 . Результат для du стал больше, что согласуется c комментарием @avp. Если выключить --apparent-size, то du возвращает занимаемое место на диске: $ du -s -B1 . 16384 . Что ещё больше отличается от суммарного размера файлов. 2) Какие могут быть "крайние" случаи, на которых программу стоит протестировать? Ну кроме пустого каталога. Потестировать имена файлов, директорий, начинающихся на точку (.zshrc, .ssh). Потестировать на директории со специальными файлами, например, /dev директория может содержать /dev/sda файл, который не является обычным файлом (это диск -- блочное устройство S_ISBLK) или FIFO (S_ISFIFO) (можно создать командой: mkfifo /tmp/named_pipe). Или потестировать на директориях с нечитаемыми записями, например, из-за недостатка прав доступа (командой chmod можно подготовить). И, конечно, потестировать на директориях, содержащих символические ссылки (S_ISLNK), которые ссылаются как на обычные файлы так и на другие директории. Для проверки надёжности, можно сгенерировать глубоко-вложенные директории с именами записей разной длины, состоящих из произвольных байтов (всё кроме слэша / и нулевого байта '\0', если локальная система не вносит своих ограничений). С, nftw() Для сравнения, можно посмотреть на примеры кода на С/С++. Для рекурсивного обхода дерева директорий, можно nftw() использовать: #define _XOPEN_SOURCE 500 #include #include #include #include int main(int argc, char* argv[]) { if (argc > 2) { fputs("Usage: get-tree-size []\n", stderr); exit(2); } int flags = FTW_PHYS; // do not follow symlinks int nopenfd = 100; // maximum number of directories that nftw() may hold // open simultaneously uintmax_t size = 0; int visit_path(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) // nested function -- gcc extension { if (typeflag == FTW_F) // regular file size += sb->st_size; return 0; // continue }; const char *dirpath = (argc == 2) ? argv[1] : "."; // default is the current working directory if (nftw(dirpath, visit_path, nopenfd, flags)) exit(1); return printf("%" PRIuMAX "\n", size) < 0; } Пример: $ gcc get-tree-size-ftw.c && ./a.out Чтобы передать дополнительные переменные (size) в visit_path() обратный вызов, gcc позволяет использовать вложенные функции. В общем случае, для более тонкого контроля обхода дерева директорий, к примеру, чтобы пропустить всё внутри .git и других подобных директорий, есть fts_open() API, добавив fnmatch() API, можно реализовать аналог команды: $ find -name .git -prune -o -type f -name \*.txt -print С, readdir() При желании, можно руками с помощью readdir() рекурсивный обход директории выполнить: #define _XOPEN_SOURCE 700 #include #include #include #include #include #include static uintmax_t get_tree_size_readdir(int dirfd) { DIR *dirp; if (!(dirp = fdopendir(dirfd))) return 0; //NOTE: ignore errors from open,openat,fdopendir here uintmax_t size = 0; for (struct dirent* entry; (entry = readdir(dirp)); ) { // skip ".", ".." entries size_t namelen = strlen(entry->d_name); if (entry->d_name[0] == '.' // 1 <= namelen <= NAME_MAX && (namelen == 1 || (entry->d_name[1] == '.' && namelen == 2))) continue; struct stat statbuf; if(fstatat(dirfd, entry->d_name, &statbuf, AT_SYMLINK_NOFOLLOW)) continue; //NOTE: ignore errors if (S_ISREG(statbuf.st_mode)) // count size of regular files only size += statbuf.st_size; else if (S_ISDIR(statbuf.st_mode)) { // count the size in subdirectories int fd = openat(dirfd, entry->d_name, O_RDONLY); size += get_tree_size_readdir(fd); //NOTE: if nested deeply; it may exceed `ulimit -n` close(fd); } } //NOTE: ignore readdir() errors closedir(dirp); //NOTE: let the caller to invoke rewinddir() if necessary return size; } директория задаётся с помощью dirfd это позволяет избежать каждый раз от корня все пути просматривать, так как entry->d_name содержит только последнюю часть пути. В противном случае пришлось бы создавать путь от входной (с которой вызов начался) директории каждый раз, прежде чем путь в stat() передать специальные имена "." и ".." явно пропускаются используется AT_SYMLINK_NOFOLLOW, чтобы не следовать по символическим ссылкам, чтобы получить информацию о самой записи (entry) ошибки по индивидуальным записям явно игнорируются #include #include int main(int argc, char* argv[]) { if (argc > 2) { fputs("Usage: get-tree-size []\n", stderr); exit(2); } const char *dirpath = (argc == 2) ? argv[1] : "."; // default is the current working directory return printf("%" PRIuMAX "\n", get_tree_size_readdir(open(dirpath, O_RDONLY))) < 0; } Пример: $ gcc get-tree-size-readdir.c && ./a.out С++, В C++ рекурсивно обойти дерево директорий можно используя библиотеку: #include #include #include namespace fs = std::experimental::filesystem; int main(int argc, char* argv[]) { if (argc > 2) { std::cerr << "Usage: get-tree-size []\n"; std::exit(2); } uintmax_t size = 0; fs::path dirpath = (argc == 2) ? argv[1] : fs::current_path(); for (auto&& entry : fs::recursive_directory_iterator( dirpath, fs::directory_options::skip_permission_denied)) { std::error_code ignore_error; if (fs::is_regular_file(fs::symlink_status(entry, ignore_error))) { size += fs::file_size(entry); } } std::cout << size << '\n'; } символические ссылки, указывающие как на директории так и на обычные файлы пропускаются "." и ".." записи также пропускаются ошибки доступа и ошибки при чтении статуса файла игнорируются, но цикл может исключения выбрасывать в случае других ошибок. Пример: $ g++ -std=c++11 *.cc -lstdc++fs && ./a.out (для ). В С++17 можно просто #include использовать. Библиотека также доступна как #include : $ sudo apt-get install libboost-{file,}system-dev # install Boost on Ubuntu $ g++ -std=c++11 *.cc -lboost_{file,}system && ./a.out Все варианты кода для подсчёта суммарного размера выдают один и тот же результат в обычных случаях, но возможны отличия, когда исключительные ситуации по разному обрабатываются. Производительность ограничивается скоростью диска. Если мета-данные уже закэшированы в памяти, то вариант с медленнее, чем nftw() и readdir(), которые похоже себя ведут. код только немного медленнее кода на Питоне.

Ответ 2



du -s /your/path/* Где /your/path/ - путь к папке. Размер указывается в килобайтах. Если нужно в байтах, то: du -sb /your/path/*

Комментариев нет:

Отправить комментарий