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

Stanislav

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

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

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

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

2 ответа

Stanislav

Чтобы подсчитать суммарный размер обычных файлов (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 <ftw.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
  if (argc > 2) {
    fputs("Usage: get-tree-size [<root-dir>]\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
  ********* 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;
}
</root-dir></stdlib.h></stdio.h></inttypes.h></ftw.h>

Пример: $ 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 <dirent.h>
#include <sys stat.h="">
#include <fcntl.h>
#include <unistd.h>
#include <inttypes.h>
#include <string.h>

static ********* get_tree_size_readdir(int dirfd)
{
  DIR *dirp;
  if (!(dirp = fdopendir(dirfd)))
    return 0; //NOTE: ignore errors from open,openat,fdopendir here
  ********* 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;
}
</string.h></inttypes.h></unistd.h></fcntl.h></sys></dirent.h>
  • директория задаётся с помощью dirfd это позволяет избежать каждый раз от корня все пути просматривать, так как entry->d_name содержит только последнюю часть пути. В противном случае пришлось бы создавать путь от входной (с которой вызов начался) директории каждый раз, прежде чем путь в stat() передать
  • специальные имена "." и ".." явно пропускаются
  • используется AT_SYMLINK_NOFOLLOW, чтобы не следовать по символическим ссылкам, чтобы получить информацию о самой записи (entry)
  • ошибки по индивидуальным записям явно игнорируются
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
  if (argc > 2) {
    fputs("Usage: get-tree-size [<root-dir>]\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;
}
</root-dir></stdlib.h></stdio.h>

Пример:

$ gcc get-tree-size-readdir.c && ./a.out

С++,

В C++ рекурсивно обойти дерево директорий можно используя <filesystem> библиотеку:

#include <experimental filesystem="">
#include <system_error>
#include <iostream>

namespace fs = std::experimental::filesystem;

int main(int argc, char* argv[])
{
  if (argc > 2) {
    std::cerr << "Usage: get-tree-size [<root-dir>]\n";
    std::exit(2);
  }
  ********* 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';
}
</root-dir></iostream></system_error></experimental>
  • символические ссылки, указывающие как на директории так и на обычные файлы пропускаются
  • "." и ".." записи также пропускаются
  • ошибки доступа и ошибки при чтении статуса файла игнорируются, но цикл может исключения выбрасывать в случае других ошибок.

Пример: $ g++ -std=c++11 *.cc -lstdc++fs && ./a.out (для <experimental/filesystem>). В С++17 можно просто #include <filesystem> использовать. Библиотека также доступна как #include <boost/filesystem.hpp>:

$ sudo apt-get install libboost-{file,}system-dev # install Boost on Ubuntu $ g++ -std=c++11 *.cc -lboost_{file,}system && ./a.out

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

Производительность ограничивается скоростью диска. Если мета-данные уже закэшированы в памяти, то вариант с <filesystem> медленнее, чем nftw() и readdir(), которые похоже себя ведут. <filesystem> код только немного медленнее кода на Питоне.


Stanislav

du -s /your/path/*

Где /your/path/ - путь к папке. Размер указывается в килобайтах.

Если нужно в байтах, то:

du -sb /your/path/*

licensed under cc by-sa 3.0 with attribution.