[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Пример скрипта отбора файлов (было Re: backup solution)



Здравствуй, дорогой All! Если ты занят и тебе не интересно - немедленно
сотри это сообщение. Если же нет, то я буду рад с тобой это обсудить.

*****

Предположим, что у нас имеется некое поддерево в linux-овой 
файловой системе (ext2,ext3,...). Важно, что на ней имеются inodes
и всякая операция изменяющая атрибуты файла (включая данные) 
приводит к тому, что ctime "помечается для обновления" (в том числе
и при переименовании файла, что жестко posix-ом не устанавливается).

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

Первая мысль, которая приходит в голову - запустить
find /part/to/subtree -cnewer /path/to/timestamp/file
и делать что-то с этими файлами. find действительно покажет нам
файлы, которые изменялись за период с момента, когда мы создали
/path/to/timestamp/file с точки зрения ядра (то есть в них что-то
писали, меняли права/владельцев и т. п.), но как быть если в список
попадает каталог? Если у него изменились права доступа, то вроде все
нормально, а если его переименовали? С точки зрения пользователя или
пользовательского приложения все файлы в этом каталоге изменились
(приложения получают доступ к файлам по пути к ним), тогда как
timestamp-ы у этих файлов вполне могли остаться "старыми".

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

- Если родительский каталог на момент предыдущего прогона существовал, то
  если этот каталог был переименован в то место в иерархии, которое занимал
  другой каталог, то у нас изменится inode number;

- Тот же inode number для другого каталога мы можем получить только одним
  способом: стереть старый каталог (освободить inode), создать по крайне 
  маловероятной случайности пучтой каталог в том же месте, с тем же именем
  и inode number, но поскольку каталог создается пустым, то любой файл, 
  который мы в нем найдем будет обновленным с точки зрения ядра (по 
  значению ctime).

/* К делу не относится: если в каталоге есть хоть один файл, который не
   был изменен с точки зрения файловой системы, то, зная имена и inode
   numbers каталогов на момент предыдущего "прогона" программы, мы можем 
   установить его старое имя
*/

То есть при каждом проходе программы мы сохраняем в snapshoot файл строчки
вида:

путь/к/каталогу/от/корня/обрабатываемого/поддерева/с/именем'/'<inode number>

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

На основании результатов сравнения мы устанавливаем флаг, говорящий о том
является ли родительский каталог перемещенным/новым, и на основании этого 
флага считаем файл обновленным безусловно (родительский каталог
переименован) или на основании сравнения ctime с записанным ctime от
прошлого прогона.

/* К делу не относится 2: если ядро не помечает для обновления ctime файла
   при обработке сист. вызова rename (posix поведения не регламентирует и
   это не случай linux), то подобная логика должна применяться ко всем 
   файлам, а не только к каталогам
*/

Вот вроде бы и все о методе отбора изменившихся файлов. Теперь о реализации.

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

1. Вызвать по ссылке &$fileproc подпрограмму, выполняющую желаемые действия
   с файлом (или его именем) со следующими параметрами:
   - путь к файлу от корня обрабатываемого поддерева (включая имя) $filename
   - признак того, что программа считает файл "измененным" $fileisnewer.
     Вычисляется на основе того, что либо родительский каталог был
     переименован/заново создан ($curparent->{unknown}), либо ctime файла
     ($ctime) не меньше записанного от предыдущего прогона ($oldtime,
     хранится в первой строчке старого snapshoot-файла)
   - тип файла ($filetype) - символ: f - regular file, l - symlink,
     d - каталог, o - другой.
2. Если файл является каталогом (и у программы есть права на его чтение и
   исполнение):
   2.1 Запомнить информацию в файле $NewSnapshoot (для корня поддерева вместо
       имени выводится время начала прогона).
   2.2 На основании поиска файла по имени в файле $workSnapshoot от 
       предыдущего прогона установить флаг того, что каталог неизвестный
       (переименованный или вновь созданный) $dirunknown
   2.3 Перейти к обработке содержимого каталога.
3. Если все файлы в каталоге обработаны вернуться в родительский каталог.

Для каждого уровня иерархии каталогов процедура getlevel заполняет
структуру, на которую указывает переменная $curparent. Поля:

 name     - путь к каталогу от корня обрабатываемого поддерева;
 entries  - отсортированный (про сортировку ниже) массив directory entries
 unknown  - признак переименованности/вновьсозданности каталога
 entnum   - размер массива entries
 entindex - текущая позиция в этом массиве
 parent   - ссылка на такую же структуру родительского каталога или 0
            для корня поддерева.

Поскольку искать строчку в неупорядоченном файле $WorkSnapshoot было бы
долго для синхронизации между прогонами используется сортировка по имени
файла. Сортируется по кодопозиционному правилу с использованием директивы
no locale. Дополнительно поскольку содержимое каталога по порядку обхода
следует сразу за ним для сравнения путей (строк, содержащих '/') 
используется замена '/' -> '\x00' (переменные $trfilename и $troldname).
Сортировка по локали игнорирует некоторые символы и поэтому от нее
пришлось отказаться во избежание труднопредсказуемых глюков.

Прилагаемый пример обходит мой домашний каталог '/home/dima', читает старый
файл snapshoot из ./wsnsh и пишет новый в ./nsnsh. При отсутствии snapshoot
от предыдущего запуска она просто выводит полный список файлов на stdout, при
его наличии - список изменившихся. 

Программа не рассчитана на использование LVM snapshoot и, вероятно, есть 
смысл передавать timestamp прогона в параметрах функции findnewer.

Функция setfileproc понимает два или три параметра: 

список типов файлов, имена которых нужно выводить,
имя файла, в который выводить обновленные файлы, и
(опционально) имя файла, в который выводить вообще все файлы указанных типов.

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

Не знаю, что еще написать...

О!!!!!
Стандартное предупреждение:

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

==========
#!/usr/bin/perl -w
use strict;
use integer;
sub getlevel {
    no locale;
    my ($dirunknown,$path2tree,$Dirname) = @_[0..2];
    my $parent = (scalar @_ > 3) ? $_[3] : 0;
    opendir(Dir,"${path2tree}/$Dirname");
    my $level = { name => $Dirname,
	  entries => [ sort grep {$_ ne '.' && $_ ne '..'} readdir(Dir) ],
	  unknown => $dirunknown,
	  entnum => 0,
	  entindex => 0,
	  parent => $parent
	  };
    closedir Dir;
    $level->{entnum} = scalar @{$level->{entries}};
    return $level;
};
sub setfileproc ($$:$) {
    my ($TypeMask,$NewerFilesListName) = @_[0,1];
    my ($FullFilesList,$NewerFilesList);
    my $FullFilesListName = '';
    if (scalar @_ > 2) {
	$FullFilesListName = shift @_;
	open $FullFilesList,">$FullFilesListName";
    };
    open $NewerFilesList,">$NewerFilesListName";
    return sub ($$$) {
	my ($filename,$fileisnewer,$filetype) = @_;
	if ((index $TypeMask,$filetype,0) >= $[ ) {
	    print $FullFilesList "$filename\n" if ($FullFilesListName ne '');
	    print $NewerFilesList "$filename\n" if $fileisnewer;
	};
    };

};
sub findnewer ($$$:$) {
    no locale;
    my ($treepath,$NewSnapshootName,$fileproc) = @_[0..2];
    $treepath =~s@/$@@ if $treepath =~ m@.+/$@;
    my ($WorkSnapshoot,$filetype,$cnewer,$ctime);
    my ($oldname,$troldname,$oldino,$oldtime,$WorkSnapshootName) = ('','',0,0,'');
    if (scalar @_ > 3) { 
	$WorkSnapshootName= $_[3]; 
	($oldtime,$oldino)= <$WorkSnapshoot> =~ m@^(.+)/(\d+)$@o if (open $WorkSnapshoot,"<$WorkSnapshootName");
    }; 
    open my $NewSnapshoot, ">$NewSnapshootName" or die "Can't open file to write new snapshoot";
    my $ino = (lstat($treepath))[1];
    my $newtime = time;
    print $NewSnapshoot "${newtime}/$ino\n";
    my $dirunknown = $ino != $oldino;
    my $curparent=&getlevel($dirunknown,$treepath,'');
    $treepath .= '/' unless ($treepath eq '' || $treepath eq '/');
    while (($curparent->{entindex} < $curparent->{entnum}) || ref $curparent->{parent}) {
	while ($curparent->{entindex} < $curparent->{entnum}) {
	    my $filename = $curparent->{entries}[$curparent->{entindex}++];
	    $filename = $curparent->{name} . $filename;
	    ($ino,$ctime) = (lstat($treepath . $filename))[1,10];
	    $cnewer = $ctime >= $oldtime;
	    $filetype = 'o';
	    $filetype = (-r _ && -x _) ? 'd' : 'D' if -d _;
	    $filetype = 'f' if -f _;
	    $filetype = 'l' if -l _;
	    my $fileisnewer = ($curparent->{unknown} || $cnewer);
	    &$fileproc($filename,$fileisnewer,($filetype =~ s/D/d/o));
	    if ($filetype eq 'd') {
		print $NewSnapshoot "${filename}/$ino\n";
		(my $trfilename = $filename) =~ tr[/][\x00];
		my $dircmp = $troldname cmp $trfilename;
		while ($dircmp < 0 && ($WorkSnapshootName ne '') && (not eof($WorkSnapshoot))) {
		    ($oldname,$oldino) = <$WorkSnapshoot> =~ m@^(.+)/(\d+)$@o;
		    ($troldname = $oldname) =~ tr[/][\x00];
		    $dircmp = $troldname cmp $trfilename;
		};
		$dirunknown = ($dircmp != 0 || $ino != $oldino);
		$curparent = &getlevel($dirunknown,$treepath,$filename,$curparent);
		$curparent->{name} .= '/';
	    };
	};
	$curparent = $curparent->{parent} if (ref $curparent->{parent});
    };
};
my $WorkSnapshootName = 'wsnsh';
my $NewSnapshootName = 'nsnsh';
my $treepath = '/home/dima';
my $types = 'dflo';
my $fileproc = &setfileproc($types,'-');
&findnewer($treepath,$NewSnapshootName,$fileproc,$WorkSnapshootName);
==========

WBR
Dmitri Ivanov



Reply to: