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:
| M | lumia.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>