lumia

Archive checksum manager
git clone git://lumidify.org/git/lumia.git
Log | Files | Refs

commit 3d69b3355b084a09aaa2a64a4b7d47e9296d4b01
parent 9c5afc6b5d09f269a33c30bb8567157e1967533a
Author: lumidify <nobody@lumidify.org>
Date:   Mon, 23 Mar 2020 14:24:40 +0100

More fixes; add some documentation

Diffstat:
Mlumia.pl | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mtest/.lumidify_archive_cksums.cksum | 2+-
Mtest/.lumidify_archive_dirs | 1-
Mtest/dir2/.lumidify_archive_cksums.cksum | 2+-
Mtest/dir2/.lumidify_archive_dirs | 1+
Rtest/dir/.lumidify_archive_cksums -> test/dir2/dir/.lumidify_archive_cksums | 0
Rtest/dir/.lumidify_archive_cksums.cksum -> test/dir2/dir/.lumidify_archive_cksums.cksum | 0
Rtest/dir/.lumidify_archive_dirs -> test/dir2/dir/.lumidify_archive_dirs | 0
Rtest/dir/meh -> test/dir2/dir/meh | 0
9 files changed, 100 insertions(+), 22 deletions(-)

diff --git a/lumia.pl b/lumia.pl @@ -28,18 +28,32 @@ use File::Basename qw(basename dirname); use File::Copy qw(move copy); use File::Path qw(remove_tree make_path); use String::ShellQuote; -use Cwd qw(getcwd); +use Cwd qw(realpath); use POSIX qw(SIGINT); use Data::Dumper; use Scalar::Util qw(looks_like_number); use Getopt::Long; +my $CKSUM_CMD = 'cksum -q'; +my %SPECIAL_FILES = ( + ".lumidify_archive_cksums" => 1, + ".lumidify_archive_cksums.cksum" => 1, + ".lumidify_archive_ignore" => 1, + ".lumidify_archive_dirs" => 1 +); + +# escape a filename for writing into the checksum files sub escape_filename { my $file = shift; $file =~ s/\\/\\\\/g; $file =~ s/"/\\"/g; return $file; } + +# make a generic file iterator +# $file_func determines whether a file should be returned by the iterator +# $dir_func is called for each directory and returns all files that +# should be added to the queue sub make_file_iter { my ($file_func, $dir_func, @queue) = @_; return sub { @@ -56,6 +70,8 @@ sub make_file_iter { }; } +# make a basic filename iterator, which simply returns all files +# for which $file_func returns a true value sub make_file_iter_basic { my ($file_func, @files) = @_; make_file_iter $file_func, sub { @@ -70,10 +86,15 @@ sub make_file_iter_basic { }, @files; } +# make a basic directory iterator, which only returns all directories sub make_dir_iter { make_file_iter_basic sub {-d $_[0]}, @_; } +# make an interator that only returns the directories which are present +# in the .lumidify_archive_dirs files +# note: this returns nonexistent directories if those are still +# specified in the lumia files sub make_lumia_iter { make_file_iter sub {1}, sub { my $path = "$_[0]/.lumidify_archive_dirs"; @@ -93,14 +114,7 @@ sub make_lumia_iter { }, @_; } -my $CKSUM_CMD = 'cksum -q'; -my %SPECIAL_FILES = ( - ".lumidify_archive_cksums" => 1, - ".lumidify_archive_cksums.cksum" => 1, - ".lumidify_archive_ignore" => 1, - ".lumidify_archive_dirs" => 1 -); - +# remove all special lumia files from the given directory sub clean_files { my $dir = shift; my $match_lumia_files = sub { @@ -116,6 +130,11 @@ sub clean_files { } } +# read a file, processing each line with $handle_cksum_func if set +# and writing the results into $cksums +# $handle_cksum_func must return two values, the checksum of the +# argument and the rest of the string (that is then parsed for +# the filename); if it returns undef, this function also returns undef sub read_file { my ($file, $cksums, $handle_cksum_func) = @_; my $fh; @@ -164,6 +183,7 @@ sub read_file { return $cksums; } +# read a single checksum file, writing the checksums into the hash $cksums and returning it sub read_cksum_file { my ($file, $cksums) = @_; return read_file $file, $cksums, sub { @@ -179,6 +199,7 @@ sub read_cksum_file { }; } +# read the checksums and directory names in $dir sub read_cksums { my $dir = shift; my $cksums = read_cksum_file("$dir/.lumidify_archive_cksums", {}); @@ -188,6 +209,8 @@ sub read_cksums { return $cksums; } +# get the checksum output for $path +# returns undef if $CKSUM_CMD returns an error sub get_cksum { my $path = shift; my $path_esc = shell_quote $path; @@ -200,6 +223,8 @@ sub get_cksum { return $cksum_output; } +# check the checksums in $dir/$cksum_file +# if $quiet is set, only print failed files sub check_cksums { my ($dir, $cksum_file, $quiet) = @_; my $cksums = read_cksum_file("$dir/$cksum_file", {}); @@ -219,6 +244,7 @@ sub check_cksums { return $failed; } +# check the checksums of all files in $top_dir sub check_files { my $top_dir = shift; my $iter = make_lumia_iter $top_dir; @@ -228,6 +254,8 @@ sub check_files { } } +# write the checksums of the special lumia files given as arguments +# to ".lumidify_archive_cksums.cksum" in $dir sub write_special_cksums { my ($dir, @files) = @_; my $cksum_file = "$dir/.lumidify_archive_cksums.cksum"; @@ -244,6 +272,13 @@ sub write_special_cksums { write_file($cksum_file, $cksums, 1); } +# search for new files that aren't present in the checksum files +# - if $file_func is set, it is called for each new file +# - if $before_dir_func is set, it is called before processing the +# files in each directory that has new files OR if a directory +# is entirely new (well, it only checks if ".lumidify_archive_cksums.cksum" exists) +# - if $after_dir_func is set, it is called after processing the +# files in each directory that has new files sub check_new_files { my ($dir, $file_func, $before_dir_func, $after_dir_func) = @_; my $iter = make_file_iter sub {-d $_[0]}, sub { @@ -294,6 +329,7 @@ sub check_new_files { while ($iter->()) {} } +# add all new files in $top_dir to the checksum files sub check_add_new_files { my $top_dir = shift; my $changed_dirs = 0; @@ -347,6 +383,9 @@ sub check_add_new_files { }; } +# write the "checksums" in $contents to $path +# if $is_cksum_file is set, the value each of the keys in $contents points +# to is written before the key sub write_file { my ($path, $contents, $is_cksum_file) = @_; my $fh; @@ -363,11 +402,16 @@ sub write_file { close $fh; } +# write the checksums in $contents to the file at $path sub write_cksum_file { my ($path, $contents) = @_; write_file $path, $contents, 1; } +# write the checksums in $contents to $dir +# any keys that point to undef are taken to be directories and vice versa +# $files_modified and $dirs_modified control which of the special lumia +# files actually get written sub write_cksums { my ($dir, $contents, $files_modified, $dirs_modified) = @_; # No, this isn't efficient... @@ -383,6 +427,7 @@ sub write_cksums { } } +# show all files that are present in the checksum files but don't exist on the filesystem anymore sub check_old_files { my $top_dir = shift; my $iter = make_lumia_iter $top_dir; @@ -399,6 +444,8 @@ sub check_old_files { } } +# clean up the lumia checksum files, removing any files that aren't present +# on the filesystem anymore sub remove_old_files { my $top_dir = shift; my $iter = make_lumia_iter $top_dir; @@ -431,6 +478,10 @@ sub remove_old_files { } } +# sort the given paths into hash based on the dirname +# returns: a hash with the keys being the dirnames of the given paths and +# each one pointing to an array containing the basenames of all paths +# that had this dirname sub sort_by_dir { my %sorted_files; foreach my $file (@_) { @@ -447,6 +498,7 @@ sub sort_by_dir { return \%sorted_files; } +# copies the $src files to $dst and updates the checksums in $dst # $src: list of source paths # $dst: destination directory or file (in latter case only one src is allowed) sub copy_files { @@ -494,14 +546,15 @@ sub copy_files { write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched; } +# return whether the two paths are the same sub cmp_path { my ($src, $dst) = @_; - # remove trailing slash so compare works - $src =~ s/\/$//; - $dst =~ s/\/$//; - return $src eq $dst; + my $src_real = realpath $src; + return defined $src_real && $src_real eq realpath $dst; } +# move a file from $src to $dst, prompting for confirmation if $dst already exists +# automatically appends the basename of $src to $dst if $dst is a directory sub move_file { my ($src, $dst) = @_; if (-d $dst) { @@ -519,6 +572,9 @@ sub move_file { return 0; } +# move all files/directories in $src_files from $src_dir to $dst_dir ($src_files +# only contains the basenames of the files), removing them from the checksum files +# in $src_dir and adding them to $dst_cksums sub move_from_same_dir { my ($src_dir, $src_files, $dst_cksums, $dst_dir) = @_; my $src_cksums = read_cksums $src_dir; @@ -531,15 +587,21 @@ sub move_from_same_dir { warn "ERROR: can't move \"$fullpath\" into \"$dst_dir\" (same dir)\n"; next; } + my $tmp_dirs_touched = 0; + my $tmp_files_touched = 0; + if (-d $fullpath) { + $tmp_dirs_touched = 1; + } else { + $tmp_files_touched = 1; + } if (my $err = move_file($fullpath, $dst_dir)) { warn "$err\n"; next; } - if (-d $fullpath) { - $dirs_touched = 1; - } else { - $files_touched = 1; - } + # need to be able to check if the path is a directory + # before actually moving it + $dirs_touched ||= $tmp_dirs_touched; + $files_touched ||= $tmp_files_touched; if (exists $src_cksums->{$src_file}) { $dst_cksums->{$src_file} = $src_cksums->{$src_file}; delete $src_cksums->{$src_file}; @@ -551,6 +613,7 @@ sub move_from_same_dir { return ($files_touched, $dirs_touched); } +# rename a single file or directory from $src to $dst sub move_rename { my ($src, $dst) = @_; my $src_dir = dirname $src; @@ -594,6 +657,12 @@ sub move_rename { } } +# move all files and directories in $src to $dst +# - if $dst does not exist, $src is only allowed to contain one path, which is +# renamed to $dst +# - if $dst is a file, $src is only allowed to contain a single path (which +# must be a file), which is renamed to $dst +# - otherwise, all files and directories in $src are moved to $dst # $src: list of source paths # $dst: destination directory or file (in latter case only one src is allowed) sub move_files { @@ -631,6 +700,7 @@ sub move_files { write_cksums $dst, $dst_cksums, $files_touched, $dirs_touched; } +# remove a file or directory from the filesystem sub remove_file_dir { my $path = shift; if (-d $path) { @@ -641,6 +711,9 @@ sub remove_file_dir { return 0; } +# remove all files in one directory, updating the checksum files in the process +# note: the files are only allowed to be basenames, i.e., they must be the +# actual filenames present in the checksum files sub remove_from_same_dir { my ($dir, @files) = @_; my $cksums = read_cksums $dir; @@ -670,6 +743,8 @@ sub remove_from_same_dir { write_cksums $dir, $cksums, $files_touched, $dirs_touched; } +# remove all given files and directories, updating the appropriate checksum +# files in the process sub remove_files { my $sorted_files = sort_by_dir(@_); foreach my $dir (keys %$sorted_files) { @@ -677,6 +752,9 @@ sub remove_files { } } +# create the given directories, initializing them with empty checksum files +# note: does not work like "mkdir -p", i.e., the new directories have to +# be located inside already existing directories sub make_dirs { my @created_dirs; foreach (@_) { @@ -726,7 +804,7 @@ sub extract { } if ($#ARGV < 0) { - die("USAGE: test.pl {init|check|clean|checknew|addnew|cp|mv|rm|mkdir}\n"); + die("USAGE: test.pl {init|check|clean|checknew|addnew|checkold|rmold|extract|cp|mv|rm|mkdir}\n"); } if ($ARGV[0] eq "mv") { if ($#ARGV < 2) { diff --git a/test/.lumidify_archive_cksums.cksum b/test/.lumidify_archive_cksums.cksum @@ -1,2 +1,2 @@ 2507213385 41 ".lumidify_archive_cksums" -1201997706 27 ".lumidify_archive_dirs" +3971863640 21 ".lumidify_archive_dirs" diff --git a/test/.lumidify_archive_dirs b/test/.lumidify_archive_dirs @@ -1,4 +1,3 @@ -"dir" "dir2" "dir3" "dir4" diff --git a/test/dir2/.lumidify_archive_cksums.cksum b/test/dir2/.lumidify_archive_cksums.cksum @@ -1,2 +1,2 @@ 4294967295 0 ".lumidify_archive_cksums" -4294967295 0 ".lumidify_archive_dirs" +137730780 6 ".lumidify_archive_dirs" diff --git a/test/dir2/.lumidify_archive_dirs b/test/dir2/.lumidify_archive_dirs @@ -0,0 +1 @@ +"dir" diff --git a/test/dir/.lumidify_archive_cksums b/test/dir2/dir/.lumidify_archive_cksums diff --git a/test/dir/.lumidify_archive_cksums.cksum b/test/dir2/dir/.lumidify_archive_cksums.cksum diff --git a/test/dir/.lumidify_archive_dirs b/test/dir2/dir/.lumidify_archive_dirs diff --git a/test/dir/meh b/test/dir2/dir/meh