lumia

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

commit f35b80dd2c875acfa769b7f1e69346c43e8cdf27
parent fae257684466739cf66a0f1d32f38e86c41c89fd
Author: lumidify <nobody@lumidify.org>
Date:   Tue, 24 Mar 2020 11:02:16 +0100

Lots of small fixes; more documentation; better option parsing

Diffstat:
Mlumia.pl | 289+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
1 file changed, 178 insertions(+), 111 deletions(-)

diff --git a/lumia.pl b/lumia.pl @@ -2,8 +2,6 @@ # FIXME: some way to avoid writing .lumidify* in dirs but still index them? e.g. Code/CMSG # FIXME: cksum don't create malformed line if permission denied -# FIXME: make generic function to traverse dirs and call other function on each dir -# FIXME: handle rm, etc. on .lumidify* files # FIXME: ignore all except for a certain file/folder # FIXME: store modified date and checksum filed with changed date # FIXME: allow different hash types @@ -18,6 +16,7 @@ use File::Basename qw(basename dirname); use File::Path qw(remove_tree); use String::ShellQuote; use Pod::Usage; +use Getopt::Std; # the file used to store checksums for files my $CKSUM_FILE = ".lumidify_archive_cksums"; @@ -110,9 +109,9 @@ sub clean_files { my $iter = make_file_iter_basic sub {exists $SPECIAL_FILES{basename $_[0]};}, $dir; while (my $file = $iter->()) { if (!unlink $file) { - warn "WARNING: Unable to remove file \"$file\"!"; + warn "WARNING: Unable to remove file \"$file\"!\n"; } else { - print "Deleted \"$file\""; + print "Deleted \"$file\"\n"; } } } @@ -231,35 +230,40 @@ sub check_cksums { return $failed; } -# check the checksums of all files in $top_dir +# check the checksums of all files and directories in @dirs sub check_files { - my $iter = make_lumia_iter @_; - while (my $file = $iter->()) { + my $args = shift; + my @dirs; + foreach my $file (@_) { if (-d $file) { - check_cksums $file, $DOUBLE_CKSUM_FILE; - check_cksums $file, $CKSUM_FILE; + push @dirs, $file; + next; + } + my $dir = dirname $file; + my $base = basename $file; + if (exists $SPECIAL_FILES{$base}) { + warn "ERROR: File is reserved for lumia.pl: $file\n"; + next; + } + my $cksums = read_cksum_file("$dir/$CKSUM_FILE"); + next if !defined $cksums; + if (!exists $cksums->{$base}) { + warn "ERROR: File doesn't exist in checksums: $file\n"; + next; + } + my $output = get_cksum "$file"; + next if !defined $output; + if ($output eq $cksums->{$base}) { + print "OK $file\n" if !$args->{"q"}; } else { - my $dir = dirname $file; - my $base = basename $file; - if (exists $SPECIAL_FILES{$base}) { - warn "ERROR: File is reserved for lumia.pl: $file\n"; - next; - } - my $cksums = read_cksum_file("$dir/$CKSUM_FILE"); - next if !defined $cksums; - if (!exists $cksums->{$base}) { - warn "ERROR: File doesn't exist in checksums: $file\n"; - next; - } - my $output = get_cksum "$file"; - next if !defined $output; - if ($output eq $cksums->{$base}) { - print "OK $file\n"; - } else { - print "FAILED $file\n"; - } + print "FAILED $file\n"; } } + my $iter = make_lumia_iter @dirs; + while (my $file = $iter->()) { + check_cksums $file, $DOUBLE_CKSUM_FILE, $args->{"q"}; + check_cksums $file, $CKSUM_FILE, $args->{"q"}; + } } # write the checksums of the special lumia files given as arguments @@ -289,7 +293,7 @@ sub write_special_cksums { # 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 { + my $iter = make_file_iter sub {1}, sub { my $dir = shift; my $dh; if (!opendir $dh, $dir) { @@ -421,18 +425,38 @@ sub write_cksum_file { # 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 +# note: this doesn't use write_file, etc. in order to (possibly) be a bit more efficient sub write_cksums { my ($dir, $contents, $files_modified, $dirs_modified) = @_; # No, this isn't efficient... + my @special_files; + my $dirs_fh; + my $files_fh; if ($files_modified) { - my %file_cksums = map {$_ => $contents->{$_}} grep({defined $contents->{$_}} keys %$contents); - write_cksum_file("$dir/$CKSUM_FILE", \%file_cksums); - write_special_cksums $dir, $CKSUM_FILE; + my $path = "$dir/$CKSUM_FILE"; + if (!open $files_fh, ">", $path) { + warn "ERROR: Unable to open \"$path\" for writing!"; + return; + } + push @special_files, $CKSUM_FILE; } if ($dirs_modified) { - my %dir_cksums = map {$_ => undef} grep({!defined $contents->{$_}} keys %$contents); - write_file "$dir/$DIR_FILE", \%dir_cksums; - write_special_cksums $dir, $DIR_FILE; + my $path = "$dir/$DIR_FILE"; + if (!open $dirs_fh, ">", $path) { + warn "ERROR: Unable to open \"$path\" for writing!"; + return; + } + push @special_files, $DIR_FILE; + } + foreach my $key (keys %$contents) { + if ($files_modified && defined $contents->{$key}) { + print $files_fh $contents->{$key} . ' "' . escape_filename($key) . '"' . "\n"; + } elsif ($dirs_modified && !defined $contents->{$key}) { + print $dirs_fh '"' . escape_filename($key) . '"' . "\n"; + } + } + if (@special_files) { + write_special_cksums $dir, @special_files; } } @@ -533,7 +557,7 @@ sub prompt_overwrite { # $src: list of source paths # $dst: destination directory or file (in latter case only one src is allowed) sub copy_files { - my ($src, $dst) = @_; + my ($src, $dst, $args) = @_; my $dst_dir = $dst; if (!-d $dst) { $dst_dir = dirname $dst; @@ -559,12 +583,17 @@ sub copy_files { my $src_path = "$src_dir/$src_file"; my $dst_path = $diff_name ? $dst : "$dst_dir/$src_file"; + if (-d $dst_path && -d $src_path) { + warn "ERROR: Cannot copy directory to already existing directory\n"; + next; + } if (exists $SPECIAL_FILES{$src_file} || exists $SPECIAL_FILES{basename $dst_path}) { warn "ERROR: Not copying special file\n"; next; } - next if prompt_overwrite($dst_path); - next if system("cp", "-av", $src_path, $dst); + next if !$args->{"f"} && prompt_overwrite($dst_path); + my $options = $args->{"v"} ? "-av" : "-a"; + next if system("cp", $options, "--", $src_path, $dst); if (-d $src_path) { $dirs_touched = 1; @@ -589,7 +618,7 @@ sub copy_files { # move a file (or directory) 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) = @_; + my ($src, $dst, $args) = @_; if (exists $SPECIAL_FILES{basename $src} || exists $SPECIAL_FILES{basename $dst}) { warn "ERROR: Not moving special file\n"; return 1; @@ -597,15 +626,26 @@ sub move_file { if (-d $dst) { $dst .= "/" . basename($src); } - return 1 if prompt_overwrite($dst); - return system("mv", $src, $dst); + return 1 if !$args->{"f"} && prompt_overwrite($dst); + my $ret; + if ($args->{"v"}) { + $ret = system("mv", "-v", "--", $src, $dst); + } else { + $ret = system("mv", "--", $src, $dst); + } + return 1 if $ret; + if (-e $src) { + warn "ERROR: file could not be removed from source but will still be " . + "removed from checksum database\n"; + } + 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_dir, $src_files, $dst_cksums, $dst_dir, $args) = @_; my $src_cksums = read_cksums $src_dir; return if !defined $src_cksums; my $files_touched = 0; @@ -620,7 +660,7 @@ sub move_from_same_dir { $tmp_files_touched = 1; } - next if move_file($fullpath, $dst_dir); + next if move_file($fullpath, $dst_dir, $args); # need to be able to check if the path is a directory # before actually moving it @@ -639,7 +679,7 @@ sub move_from_same_dir { # rename a single file or directory from $src to $dst sub move_rename { - my ($src, $dst) = @_; + my ($src, $dst, $args) = @_; my $src_dir = dirname $src; my $dst_dir = dirname $dst; my $src_file = basename $src; @@ -666,7 +706,7 @@ sub move_rename { $files_touched = 1; } - return if move_file($src, $dst); + return if move_file($src, $dst, $args); if (exists($src_cksums->{$src_file})) { $dst_cksums->{$dst_file} = $src_cksums->{$src_file}; @@ -689,16 +729,16 @@ sub move_rename { # $src: list of source paths # $dst: destination directory or file (in latter case only one src is allowed) sub move_files { - my ($src, $dst) = @_; + my ($src, $dst, $args) = @_; if (!-d $dst && $#$src != 0) { die "move: only one source argument allowed when destination is a file\n"; } if (!-d $dst && !-d $src->[0]) { - move_rename $src->[0], $dst; + move_rename $src->[0], $dst, $args; return; } if (!-e $dst && -d $src->[0]) { - move_rename $src->[0], $dst; + move_rename $src->[0], $dst, $args; return; } if (-e $dst && !-d $dst && -d $src->[0]) { @@ -711,7 +751,7 @@ sub move_files { my $files_touched = 0; my $dirs_touched = 0; foreach my $src_dir (keys %$src_files) { - my ($tmp_files_touched, $tmp_dirs_touched) = move_from_same_dir $src_dir, $src_files->{$src_dir}, $dst_cksums, $dst; + my ($tmp_files_touched, $tmp_dirs_touched) = move_from_same_dir $src_dir, $src_files->{$src_dir}, $dst_cksums, $dst, $args; $files_touched ||= $tmp_files_touched; $dirs_touched ||= $tmp_dirs_touched; } @@ -720,11 +760,14 @@ sub move_files { # remove a file or directory from the filesystem sub remove_file_dir { - my $path = shift; - if (-d $path) { - remove_tree $path, {safe => 1} or return "ERROR: can't remove \"$path\": $!"; - } else { - unlink $path or return "ERROR: can't remove \"$path\": $!"; + my ($path, $args) = @_; + my $options = $args->{"f"} ? "-rf" : "-f"; + if (system("rm", $options, "--", $path)) { + return 1; + } + if (-e $path) { + warn "ERROR: unable to remove \"$path\" from filesystem but " . + "will still be removed from checksum database\n"; } return 0; } @@ -733,7 +776,7 @@ sub remove_file_dir { # 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 ($args, $dir, @files) = @_; my $cksums = read_cksums $dir; return if !defined $cksums; my $dirs_touched = 0; @@ -747,10 +790,7 @@ sub remove_from_same_dir { if (!-e $fullpath) { warn "\"$fullpath\": No such file or directory.\n"; } - if (my $err = remove_file_dir($fullpath)) { - warn "$err\n"; - next; - } + next if remove_file_dir($fullpath, $args); if (exists $cksums->{$file}) { if (defined $cksums->{$file}) { $files_touched = 1; @@ -768,9 +808,10 @@ sub remove_from_same_dir { # remove all given files and directories, updating the appropriate checksum # files in the process sub remove_files { + my $args = shift; my $sorted_files = sort_by_dir(@_); foreach my $dir (keys %$sorted_files) { - remove_from_same_dir($dir, @{$sorted_files->{$dir}}); + remove_from_same_dir($args, $dir, @{$sorted_files->{$dir}}); } } @@ -780,7 +821,7 @@ sub remove_files { sub make_dirs { my @created_dirs; foreach (@_) { - if (system("mkdir", $_)) { + if (system("mkdir", "--", $_)) { warn "ERROR creating directory $_\n"; next; } @@ -815,11 +856,11 @@ sub extract { while (my $dir = $iter->()) { my $final_dir = abs2rel $dir, $src_dir; my $fulldir = catfile $dst_dir, $final_dir; - system("mkdir", "-p", $fulldir); + system("mkdir", "-p", "--", $fulldir); foreach my $file (keys %SPECIAL_FILES) { my $filepath = catfile $dir, $file; if (-e $filepath) { - system("cp", "-aiv", $filepath, catfile($fulldir, $file)); + system("cp", "-aiv", "--", $filepath, catfile($fulldir, $file)); } } } @@ -854,63 +895,66 @@ sub update { } } -pod2usage(-verbose => 1) if $#ARGV < 0; +my %args; +getopts("fqh", \%args); -if ($ARGV[0] eq "mv") { - if ($#ARGV < 2) { - die "mv requires at least two arguments\n"; - } - my @src = @ARGV[1..$#ARGV-1]; - move_files(\@src, $ARGV[-1]); -} elsif ($ARGV[0] eq "rm") { - if ($#ARGV < 1) { +pod2usage(-verbose => 1) if @ARGV < 1 || $args{"h"}; + +my $cmd = shift; + +if ($cmd eq "mv") { + die "mv requires at least two arguments\n" if @ARGV < 2; + my @src = @ARGV[0..$#ARGV-1]; + move_files(\@src, $ARGV[-1], \%args); +} elsif ($cmd eq "rm") { + if (@ARGV < 1) { die "rm requires at least one argument\n"; } - remove_files(@ARGV[1..$#ARGV]); -} elsif ($ARGV[0] eq "addnew") { + remove_files \%args, @ARGV; +} elsif ($cmd eq "addnew") { my $dir = "."; - if ($#ARGV > 0) { - $dir = $ARGV[1]; + if (@ARGV >= 1) { + $dir = $ARGV[0]; } check_add_new_files $dir; -} elsif ($ARGV[0] eq "checknew") { +} elsif ($cmd eq "checknew") { my $dir = "."; - if ($#ARGV > 0) { - $dir = $ARGV[1]; + if (@ARGV >= 1) { + $dir = $ARGV[0]; } check_new_files $dir; -} elsif ($ARGV[0] eq "checkold") { +} elsif ($cmd eq "checkold") { my $dir = "."; - if ($#ARGV > 0) { - $dir = $ARGV[1]; + if (@ARGV >= 1) { + $dir = $ARGV[0]; } check_old_files $dir; -} elsif ($ARGV[0] eq "rmold") { +} elsif ($cmd eq "rmold") { my $dir = "."; - if ($#ARGV > 0) { - $dir = $ARGV[1]; + if (@ARGV >= 1) { + $dir = $ARGV[0]; } remove_old_files $dir; -} elsif ($ARGV[0] eq "check") { - if ($#ARGV < 1) { - check_files "."; +} elsif ($cmd eq "check") { + if (@ARGV < 1) { + check_files \%args, "."; } else { - check_files @ARGV[1..$#ARGV]; + check_files \%args, @ARGV; } -} elsif ($ARGV[0] eq "clean") { +} elsif ($cmd eq "clean") { my $dir = "."; - if ($#ARGV > 0) { - $dir = $ARGV[1]; + if (@ARGV >= 1) { + $dir = $ARGV[0]; } clean_files $dir; -} elsif ($ARGV[0] eq "extract") { +} elsif ($cmd eq "extract") { my $src_dir = "."; my $dst_dir; - if ($#ARGV > 1) { - $src_dir = $ARGV[1]; - $dst_dir = $ARGV[2]; - } elsif ($#ARGV == 1) { - $dst_dir = $ARGV[1]; + if (@ARGV >= 2) { + $src_dir = $ARGV[0]; + $dst_dir = $ARGV[1]; + } elsif (@ARGV == 1) { + $dst_dir = $ARGV[0]; } else { die "ERROR: `extract` requires at least a destination directory.\n"; } @@ -921,24 +965,23 @@ if ($ARGV[0] eq "mv") { die "ERROR: Directory \"$dst_dir\" does not exist.\n"; } extract $src_dir, $dst_dir; -} elsif ($ARGV[0] eq "cp") { - if ($#ARGV < 2) { +} elsif ($cmd eq "cp") { + if (@ARGV < 2) { die "cp requires at least two arguments\n"; } - my @src = @ARGV[1..$#ARGV-1]; - copy_files(\@src, $ARGV[-1]); -} elsif ($ARGV[0] eq "mkdir") { - if ($#ARGV < 1) { + my @src = @ARGV[0..$#ARGV-1]; + copy_files \@src, $ARGV[-1], \%args; +} elsif ($cmd eq "mkdir") { + if (@ARGV < 1) { die "mkdir requires at least one argument\n"; } - my @dirs = @ARGV[1..$#ARGV]; - make_dirs(@dirs); -} elsif ($ARGV[0] eq "update") { - if ($#ARGV < 1) { + make_dirs @ARGV; +} elsif ($cmd eq "update") { + if (@ARGV < 1) { die "update requires at least one argument\n"; } - update @ARGV[1..$#ARGV]; -} elsif ($ARGV[0] eq "help") { + update @ARGV; +} elsif ($cmd eq "help") { pod2usage(-exitval => 0, -verbose => 2); } @@ -950,7 +993,7 @@ lumia.pl - Manage checksums on a filesystem =head1 SYNOPSIS -B<lumia.pl> command arguments +B<lumia.pl> [-qfh] command arguments =head1 OPTIONS @@ -1030,6 +1073,30 @@ into it. =back +=head1 CAVEATS + +B<rm> automatically deletes the files recursively. For each of the arguments, +the following caveats apply: +If any actual errors occur while deleting the file/directory (i.e. the system +command C<rm> returns a non-zero exit value), the checksum or directory B<is +left in the database>. If the system C<rm> does not return a non-zero exit value, +but the file/directory still exists afterwards (e.g. there was a permission +error and the user answered "n" when prompted), a warning message is printed, +but the files B<are removed from the database> (if the database can be +written to). + +B<mv> behaves the same as B<rm> with regards to checking if the source file +is still present after the operation. + +B<cp> will issue a warning and skip to the next argument if it is asked to +merge a directory with an already existing directory. For instance, attempting +to run C<cp dir1 dir2>, where C<dir2> already contains a directory named +C<dir1>, will result in an error. This may change in the future, when the +program is modified to recursively copy the files manually, instead of simply +calling the system C<cp> on each of the arguments. If this was supported in +the current version, none of the checksums inside that directory would be +updated, so it wouldn't be very useful. + =head1 LICENSE Copyright (c) 2019, 2020 lumidify <nobody[at]lumidify.org>