lumia

Archive checksum manager
git clone git://lumidify.org/lumia.git (fast, but not encrypted)
git clone https://lumidify.org/lumia.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/lumia.git (over tor)
Log | Files | Refs | README | LICENSE

lumia (45170B)


      1 #!/usr/bin/env perl
      2 
      3 # TODO: some way to avoid writing .lumidify* in dirs but still index them?
      4 # TODO: store modified date and checksum files with changed date
      5 # TODO: add option to just check dir structure or maybe check if everything exists
      6 # TODO: add option to compare cksums of two dirs
      7 # TODO: exit status!
      8 
      9 use strict;
     10 use warnings;
     11 use File::Spec::Functions qw(catfile abs2rel);
     12 use File::Basename qw(basename dirname);
     13 use String::ShellQuote;
     14 use Pod::Usage;
     15 use Getopt::Long;
     16 
     17 # the file used to store checksums for files
     18 my $CKSUM_FILE = ".lumidify_archive_cksums";
     19 # the file used to store directory names
     20 my $DIR_FILE = ".lumidify_archive_dirs";
     21 # the file read to ignore files or directories
     22 my $IGNORE_FILE = ".lumidify_archive_ignore";
     23 # the file containing checksums of $CKSUM_FILE and $DIR_FILE
     24 my $DOUBLE_CKSUM_FILE = ".lumidify_archive_cksums.cksum";
     25 
     26 # uncomment this instead of the lines below to use
     27 # sha256 instead of cksum as the hash algorithm
     28 
     29 # On OpenBSD:
     30 #my $CKSUM_CMD = 'sha256 -q';
     31 #my $CKSUM_NUMFIELDS = 1;
     32 
     33 # On systems with GNU coreutils:
     34 #my $CKSUM_CMD = 'sha256sum';
     35 #my $CKSUM_NUMFIELDS = 1;
     36 
     37 my $CKSUM_CMD = 'cksum';
     38 my $CKSUM_NUMFIELDS = 2;
     39 
     40 my %SPECIAL_FILES = (
     41 	$CKSUM_FILE => 1,
     42 	$DIR_FILE => 1,
     43 	$IGNORE_FILE => 1,
     44 	$DOUBLE_CKSUM_FILE => 1
     45 );
     46 
     47 # escape a filename for writing into the checksum files
     48 sub escape_filename {
     49 	my $file = shift;
     50 	$file =~ s/\\/\\\\/g;
     51 	$file =~ s/"/\\"/g;
     52 	return $file;
     53 }
     54 
     55 # make a generic file iterator
     56 # $file_func determines whether a file should be returned by the iterator
     57 # $dir_func is called for each directory and returns all files that
     58 # should be added to the queue
     59 sub make_file_iter {
     60 	my ($file_func, $dir_func, @queue) = @_;
     61 	return sub {
     62 		while (@queue) {
     63 			my $file = pop @queue;
     64 			if (-d $file) {
     65 				my $new_files = $dir_func->($file);
     66 				next if !defined $new_files;
     67 				push @queue, sort {$b cmp $a} @$new_files;
     68 			}
     69 			return $file if $file_func->($file);
     70 		}
     71 		return;
     72 	};
     73 }
     74 
     75 # make a basic filename iterator, which simply returns all files
     76 # for which $file_func returns a true value
     77 sub make_file_iter_basic {
     78 	my ($file_func, @files) = @_;
     79 	make_file_iter $file_func, sub {
     80 		my $dh;
     81 		if (!opendir $dh, $_[0]) {
     82 			warn "WARNING: Unable to open directory \"$_[0]\"!";
     83 			return [];
     84 		}
     85 		my @new_files = map "$_[0]/$_", grep {$_ ne "." && $_ ne ".."} readdir $dh;
     86 		closedir $dh;
     87 		return \@new_files;
     88 	}, @files;
     89 }
     90 
     91 # make an interator that only returns the directories which are present
     92 # in the $DIR_FILE files, in addition to the files and directories that
     93 # were originally passed as arguments
     94 # note: this returns nonexistent directories if those are still
     95 # specified in the lumia files
     96 sub make_lumia_iter {
     97 	my ($quiet, @dirs) = @_;
     98 	make_file_iter sub {1}, sub {
     99 		my $path = "$_[0]/$DIR_FILE";
    100 		return [] if !-f $path;
    101 		my $dirs = read_file($path, {});
    102 		return if !defined $dirs;
    103 		my @new_dirs;
    104 		foreach my $dir (keys %$dirs) {
    105 			my $dir_path = "$_[0]/$dir";
    106 			if (!-d $dir_path) {
    107 				warn "ERROR: Directory \"$dir_path\" mentioned in " .
    108 					"\"$path\" does not exist or is not directory.\n" if !$quiet;
    109 			}
    110 			# still push it even when it doesn't exist so rmold can work properly
    111 			push @new_dirs, $dir_path;
    112 		}
    113 		return \@new_dirs;
    114 	}, @dirs;
    115 }
    116 
    117 # remove all special lumia files from the given directory
    118 sub clean_files {
    119 	my ($dir, $args) = @_;
    120 	my $iter = make_file_iter_basic sub {exists $SPECIAL_FILES{basename $_[0]};}, $dir;
    121 	while (my $file = $iter->()) {
    122 		if (!unlink $file) {
    123 			warn "WARNING: Unable to remove file \"$file\"!\n";
    124 		} else {
    125 			print "Deleted \"$file\"\n" if !$args->{"q"};
    126 		}
    127 	}
    128 }
    129 
    130 # read a file, processing each line with $handle_cksum_func if set
    131 # and writing the results into $cksums
    132 # $handle_cksum_func must return two values, the checksum of the
    133 # argument and the rest of the string (that is then parsed for
    134 # the filename); if it returns undef, this function also returns undef
    135 sub read_file {
    136 	my ($file, $cksums, $handle_cksum_func) = @_;
    137 	my $fh;
    138 	if (!open $fh, "<", $file) {
    139 		warn "ERROR: Unable to open file \"$file\": $!\n";
    140 		return;
    141 	}
    142 	my $in_fn = 0;
    143 	my $cur_cksum;
    144 	my $cur_str;
    145 	my $cur_fn = "";
    146 	foreach (<$fh>) {
    147 		next if (!$in_fn && /^$/);
    148 		if ($handle_cksum_func && !$in_fn) {
    149 			($cur_cksum, $cur_str) = $handle_cksum_func->($_);
    150 			return undef if !defined $cur_cksum;
    151 		} else {
    152 			$cur_str = $_;
    153 		}
    154 		my $bs = 0;
    155 		foreach my $ch (split(//, $cur_str)) {
    156 			if ($ch eq "\\") {
    157 				$bs++;
    158 				$cur_fn .= "\\" if !($bs %= 2) && $in_fn;
    159 			} elsif ($bs % 2) {
    160 				$cur_fn .= $ch  if $in_fn;
    161 				$bs = 0;
    162 			} elsif ($ch eq "\"") {
    163 				if ($in_fn) {
    164 					$in_fn = 0;
    165 					$cksums->{$cur_fn} = $cur_cksum;
    166 					$cur_fn = "";
    167 					last;
    168 				}
    169 				$in_fn = 1;
    170 			} elsif ($in_fn) {
    171 				$cur_fn .= $ch;
    172 			}
    173 		}
    174 	}
    175 	close $fh;
    176 	if ($in_fn) {
    177 		warn "ERROR: Unterminated filename in file \"$file\"\n";
    178 		return undef;
    179 	}
    180 	return $cksums;
    181 }
    182 
    183 # read a single checksum file, writing the checksums into the hash $cksums and returning it
    184 sub read_cksum_file {
    185 	my ($file, $cksums) = @_;
    186 	return read_file $file, $cksums, sub {
    187 		my $line = shift;
    188 		my @fields = split(/ /, $line, $CKSUM_NUMFIELDS+1);
    189 		if (@fields != $CKSUM_NUMFIELDS+1) {
    190 			warn "WARNING: Malformed line \"$line\" in file \"$file\"\n";
    191 			return;
    192 		}
    193 		my $cur_cksum = join(" ", @fields[0..$CKSUM_NUMFIELDS-1]);
    194 		my $cur_str = $fields[$CKSUM_NUMFIELDS];
    195 		return ($cur_cksum, $cur_str);
    196 	};
    197 }
    198 
    199 # read the checksums and directory names in $dir
    200 sub read_cksums {
    201 	my $dir = shift;
    202 	my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {});
    203 	return undef if !defined $cksums;
    204 	$cksums = read_file("$dir/$DIR_FILE", $cksums);
    205 	return undef if !defined $cksums;
    206 	return $cksums;
    207 }
    208 
    209 # get the checksum output for $path
    210 # returns undef if $CKSUM_CMD returns an error
    211 sub get_cksum {
    212 	my $path = shift;
    213 	my $path_esc = shell_quote $path;
    214 	my $cksum_output = `$CKSUM_CMD -- $path_esc 2>&1`;
    215 	if ($?) {
    216 		warn "ERROR getting cksum for file \"$path\":\n$cksum_output";
    217 		return undef;
    218 	}
    219 	chomp $cksum_output;
    220 	my @fields = split(/ /, $cksum_output, $CKSUM_NUMFIELDS+1);
    221 	return join(" ", @fields[0..$CKSUM_NUMFIELDS-1]);
    222 }
    223 
    224 # check the checksums in $dir/$cksum_file
    225 # if $quiet is set, only print failed files
    226 sub check_cksums {
    227 	my ($dir, $cksum_file, $quiet) = @_;
    228 	my $cksums = read_cksum_file("$dir/$cksum_file", {});
    229 	return 0 if !defined $cksums;
    230 	my $failed = 1;
    231 	foreach my $file (sort keys %$cksums) {
    232 		my $path = "$dir/$file";
    233 		my $output = get_cksum $path;
    234 		next if !defined $output;
    235 		if ($output eq $cksums->{$file}) {
    236 			print "OK $path\n" if !$quiet;
    237 		} else {
    238 			print "FAILED $path\n";
    239 			$failed = 0;
    240 		}
    241 	}
    242 	return $failed;
    243 }
    244 
    245 # check the checksums of all files and directories in @dirs
    246 sub check_files {
    247 	my $args = shift;
    248 	my @dirs;
    249 	foreach my $file (sort @_) {
    250 		if (-d $file) {
    251 			push @dirs, $file;
    252 			next;
    253 		}
    254 		my $dir = dirname $file;
    255 		my $base = basename $file;
    256 		if (exists $SPECIAL_FILES{$base}) {
    257 			warn "ERROR: File is reserved for lumia: $file\n";
    258 			next;
    259 		}
    260 		my $cksums = read_cksum_file("$dir/$CKSUM_FILE");
    261 		next if !defined $cksums;
    262 		if (!exists $cksums->{$base}) {
    263 			warn "ERROR: File doesn't exist in checksums: $file\n";
    264 			next;
    265 		}
    266 		my $output = get_cksum "$file";
    267 		next if !defined $output;
    268 		if ($output eq $cksums->{$base}) {
    269 			print "OK $file\n" if !$args->{"q"};
    270 		} else {
    271 			print "FAILED $file\n";
    272 		}
    273 	}
    274 	my $iter = make_lumia_iter 0, reverse @dirs;
    275 	while (my $file = $iter->()) {
    276 		check_cksums $file, $DOUBLE_CKSUM_FILE, $args->{"q"};
    277 		check_cksums $file, $CKSUM_FILE, $args->{"q"};
    278 	}
    279 }
    280 
    281 # write the checksums of the special lumia files given as arguments
    282 # to $DOUBLE_CKSUM_FILE in $dir
    283 sub write_special_cksums {
    284 	my ($dir, @files) = @_;
    285 	my $cksum_file = "$dir/$DOUBLE_CKSUM_FILE";
    286 	my $cksums = {};
    287 	if (-f $cksum_file) {
    288 		$cksums = read_cksum_file $cksum_file, {};
    289 	}
    290 	return if !defined $cksums;
    291 	foreach my $file (sort @files) {
    292 		my $cksum_output = get_cksum("$dir/$file");
    293 		next if (!defined $cksum_output);
    294 		$cksums->{$file} = $cksum_output;
    295 	}
    296 	write_file($cksum_file, $cksums, 1);
    297 }
    298 
    299 # search for new files that aren't present in the checksum files
    300 # - if $file_func is set, it is called for each new file
    301 # - if $before_dir_func is set, it is called before processing the
    302 #   files in each directory that has new files OR if a directory
    303 #   is entirely new (well, it only checks if $DOUBLE_CKSUM_FILE exists)
    304 # - if $after_dir_func is set, it is called after processing the
    305 #   files in each directory that has new files
    306 sub check_new_files {
    307 	my ($top_dir, $file_func, $before_dir_func, $after_dir_func) = @_;
    308 	my $iter = make_file_iter sub {1}, sub {
    309 		my $dir = shift;
    310 		my $dh;
    311 		if (!opendir $dh, $dir) {
    312 			warn "ERROR: Unable to open directory \"$dir\"!";
    313 			return undef;
    314 		}
    315 		my $read_file_noerror = sub {
    316 			if (-f $_[0]) {
    317 				return $_[1]->($_[0], {}) // {};
    318 			}
    319 			return {};
    320 		};
    321 		my $ignore = $read_file_noerror->("$dir/$IGNORE_FILE", \&read_file);
    322 		my $lumia_dirs = $read_file_noerror->("$dir/$DIR_FILE", \&read_file);
    323 		my $lumia_files = $read_file_noerror->("$dir/$CKSUM_FILE", \&read_cksum_file);
    324 		my @dirs;
    325 		my $found = 0;
    326 		my @sortedfiles = sort readdir $dh;
    327 		foreach my $file (@sortedfiles) {
    328 			next if $file eq "." || $file eq "..";
    329 			next if exists $ignore->{$file} || exists $SPECIAL_FILES{$file};
    330 			# FIXME: move file from dirs to files and vice versa if necessary
    331 			if (!exists $lumia_dirs->{$file} && !exists $lumia_files->{$file}) {
    332 				if (!$found && defined $before_dir_func) {
    333 					last if !$before_dir_func->($dir);
    334 				}
    335 				if (defined $file_func) {
    336 					$file_func->($dir, $file);
    337 				} else {
    338 					print "$dir/$file\n";
    339 				}
    340 				$found = 1;
    341 			}
    342 			push @dirs, "$dir/$file" if -d "$dir/$file";
    343 		}
    344 		closedir $dh;
    345 		# FIXME: should this check for presence of other .lumidify* files as well?
    346 		# also call $before_dir_func if the directory has not been initialized yet
    347 		if (!$found && !-f "$dir/$DOUBLE_CKSUM_FILE" && defined $before_dir_func) {
    348 			$before_dir_func->($dir);
    349 		}
    350 		if ($found && defined $after_dir_func) {
    351 			$after_dir_func->($dir);
    352 		}
    353 		return \@dirs;
    354 	}, $top_dir;
    355 	# Is this a horrible hack? I dunno, but it sure is sweet...
    356 	while ($iter->()) {}
    357 }
    358 
    359 # add all new files in $top_dir to the checksum files
    360 sub check_add_new_files {
    361 	my ($top_dir, $args) = @_;
    362 	my $changed_dirs = 0;
    363 	my $changed_files = 0;
    364 	check_new_files $top_dir, sub {
    365 		my ($dir, $file) = @_;
    366 		my $fullpath = "$dir/$file";
    367 		if (-d $fullpath) {
    368 			my $dir_file = "$dir/$DIR_FILE";
    369 			my $fh;
    370 			if (!open $fh, ">>", $dir_file) {
    371 				warn "ERROR: Unable to append to file \"$dir_file\"!";
    372 				return;
    373 			}
    374 			print $fh '"' . escape_filename($file) . '"' . "\n";
    375 			close $fh;
    376 			$changed_dirs = 1;
    377 		} else {
    378 			my $cksum_output = get_cksum $fullpath;
    379 			return if !defined $cksum_output;
    380 			my $cksum_file = "$dir/$CKSUM_FILE";
    381 			my $fh;
    382 			if (!open $fh, ">>", $cksum_file) {
    383 				warn "ERROR: Unable to append to file \"$cksum_file\"!";
    384 				return;
    385 			}
    386 			print $fh $cksum_output . ' "' . escape_filename($file) . '"' . "\n";
    387 			close $fh;
    388 			$changed_files = 1;
    389 		}
    390 		print "Added \"$fullpath\"\n" if !$args->{"q"};
    391 	}, sub {
    392 		if (-f "$_[0]/$DOUBLE_CKSUM_FILE") {
    393 			if (!check_cksums $_[0], $DOUBLE_CKSUM_FILE, 1) {
    394 				warn "Checksum files corrupt in \"$_[0]\", not adding new checksums!\n";
    395 				return 0;
    396 			}
    397 
    398 		} else {
    399 			write_cksums($_[0], {}, 1, 1);
    400 		}
    401 		return 1;
    402 	}, sub {
    403 		if ($changed_dirs) {
    404 			write_special_cksums $_[0], $DIR_FILE;
    405 			$changed_dirs = 0;
    406 		}
    407 		if ($changed_files) {
    408 			write_special_cksums $_[0], $CKSUM_FILE;
    409 			$changed_files = 0;
    410 		}
    411 	};
    412 }
    413 
    414 # write the "checksums" in $contents to $path
    415 # if $is_cksum_file is set, the value each of the keys in $contents points
    416 # to is written before the key
    417 sub write_file {
    418 	my ($path, $contents, $is_cksum_file) = @_;
    419 	my $fh;
    420 	if (!open $fh, ">", $path) {
    421 		warn "ERROR: Unable to open \"$path\" for writing!";
    422 		return;
    423 	}
    424 	foreach my $filename (keys %$contents) {
    425 		if ($is_cksum_file) {
    426 			print $fh "$contents->{$filename} ";
    427 		}
    428 		print $fh '"' . escape_filename($filename) . '"' . "\n";
    429 	}
    430 	close $fh;
    431 }
    432 
    433 # write the checksums in $contents to the file at $path
    434 sub write_cksum_file {
    435 	my ($path, $contents) = @_;
    436 	write_file $path, $contents, 1;
    437 }
    438 
    439 # write the checksums in $contents to $dir
    440 # any keys that point to undef are taken to be directories and vice versa
    441 # $files_modified and $dirs_modified control which of the special lumia
    442 # files actually get written
    443 # note: this doesn't use write_file, etc. in order to (possibly) be a bit more efficient
    444 sub write_cksums {
    445 	my ($dir, $contents, $files_modified, $dirs_modified) = @_;
    446 	# No, this isn't efficient...
    447 	my @special_files;
    448 	my $dirs_fh;
    449 	my $files_fh;
    450 	if ($files_modified) {
    451 		my $path = "$dir/$CKSUM_FILE";
    452 		if (!open $files_fh, ">", $path) {
    453 			warn "ERROR: Unable to open \"$path\" for writing!";
    454 			return;
    455 		}
    456 		push @special_files, $CKSUM_FILE;
    457 	}
    458 	if ($dirs_modified) {
    459 		my $path = "$dir/$DIR_FILE";
    460 		if (!open $dirs_fh, ">", $path) {
    461 			warn "ERROR: Unable to open \"$path\" for writing!";
    462 			return;
    463 		}
    464 		push @special_files, $DIR_FILE;
    465 	}
    466 	foreach my $key (keys %$contents) {
    467 		if ($files_modified && defined $contents->{$key}) {
    468 			print $files_fh $contents->{$key} . ' "' . escape_filename($key) . '"' . "\n";
    469 		} elsif ($dirs_modified && !defined $contents->{$key}) {
    470 			print $dirs_fh '"' . escape_filename($key) . '"' . "\n";
    471 		}
    472 	}
    473 	close $files_fh if defined $files_fh;
    474 	close $dirs_fh if defined $dirs_fh;
    475 	if (@special_files) {
    476 		write_special_cksums $dir, @special_files;
    477 	}
    478 }
    479 
    480 # show all files that are present in the checksum files but don't exist on the filesystem anymore
    481 sub check_old_files {
    482 	my $top_dir = shift;
    483 	my $iter = make_lumia_iter 1, $top_dir;
    484 	while (my $dir = $iter->()) {
    485 		if (-e $dir) {
    486 			my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {}) // {};
    487 			foreach my $file (keys %$cksums) {
    488 				if (!-e "$dir/$file") {
    489 					warn "Nonexistent file: \"$dir/$file\"!\n";
    490 				}
    491 			}
    492 		} else {
    493 			warn "Nonexistent directory: \"$dir\"!\n";
    494 		}
    495 	}
    496 }
    497 
    498 # clean up the lumia checksum files, removing any files that aren't present
    499 # on the filesystem anymore
    500 sub remove_old_files {
    501 	my ($top_dir, $args) = @_;
    502 	my $iter = make_lumia_iter 1, $top_dir;
    503 	while (my $dir = $iter->()) {
    504 		if (!-e $dir) {
    505 			my $parent = dirname $dir;
    506 			my $child = basename $dir;
    507 			my $lumia_dirs = read_file("$parent/$DIR_FILE", {}) // {};
    508 			if (exists $lumia_dirs->{$child}) {
    509 				delete $lumia_dirs->{$child};
    510 				write_file "$parent/$DIR_FILE", $lumia_dirs;
    511 				print "Removed \"$dir\" from \"$parent/$DIR_FILE\"\n" if !$args->{"q"};
    512 				write_special_cksums $parent, $DIR_FILE;
    513 			}
    514 		} else {
    515 			my $cksums = read_cksum_file("$dir/$CKSUM_FILE", {}) // {};
    516 			my $found = 0;
    517 			foreach my $file (keys %$cksums) {
    518 				if (!-e "$dir/$file") {
    519 					delete $cksums->{$file};
    520 					print "Removed \"$dir/$file\" from \"$dir/$CKSUM_FILE\"\n" if !$args->{"q"};
    521 					$found = 1;
    522 				}
    523 			}
    524 			if ($found) {
    525 				write_cksum_file "$dir/$CKSUM_FILE", $cksums;
    526 				write_special_cksums $dir, $CKSUM_FILE;
    527 			}
    528 		}
    529 	}
    530 }
    531 
    532 # sort the given paths into hash based on the dirname
    533 # returns: a hash with the keys being the dirnames of the given paths and
    534 # each one pointing to an array containing the basenames of all paths
    535 # that had this dirname
    536 sub sort_by_dir {
    537 	my %sorted_files;
    538 	foreach my $file (@_) {
    539 		if (!-e $file) {
    540 			warn "ERROR: Source file \"$file\" doesn't exist.\n";
    541 			next;
    542 		}
    543 		my $dir = dirname($file);
    544 		if (!exists($sorted_files{$dir})) {
    545 			$sorted_files{$dir} = [];
    546 		}
    547 		push(@{$sorted_files{$dir}}, basename($file));
    548 	}
    549 	return \%sorted_files;
    550 }
    551 
    552 # check if $dst exists and prompt the user whether it should be overwritten
    553 # returns 0 if it can be overwritten or doesn't exist, 1 if it shouldn't be overwritten
    554 sub prompt_overwrite {
    555 	my $dst = shift;
    556 	if (-e $dst) {
    557 		print STDERR "WARNING: \"$dst\" exists already. Do you want to replace it? (y/n) ";
    558 		my $choice = "";
    559 		while ($choice ne "y" && $choice ne "n") {
    560 			$choice = <STDIN>;
    561 			chomp $choice;
    562 		}
    563 		if ($choice eq "n") {
    564 			warn "Not overwriting \"$dst\"\n";
    565 			return 1;
    566 		} else {
    567 			return 0;
    568 		}
    569 	}
    570 	return 0;
    571 }
    572 
    573 # copies the $src files to $dst and updates the checksums in $dst
    574 # $src: list of source paths
    575 # $dst: destination directory or file (in latter case only one src is allowed)
    576 sub copy_files {
    577 	my ($src, $dst, $args) = @_;
    578 	my $dst_dir = $dst;
    579 	if (!-d $dst) {
    580 		$dst_dir = dirname $dst;
    581 	}
    582 	my $diff_name = 0;
    583 	# check if the file/dir is getting a different name or
    584 	# just being copied into a different directory
    585 	if (!-d $dst && !-d $src->[0]) {
    586 		$diff_name = 1;
    587 	}
    588 	if (!-e $dst && -d $src->[0]) {
    589 		$diff_name = 1;
    590 	}
    591 	my $dst_cksums = read_cksums $dst_dir;
    592 	return if !defined $dst_cksums;
    593 	my $src_sorted = sort_by_dir(@$src);
    594 	my $files_touched = 0;
    595 	my $dirs_touched = 0;
    596 	foreach my $src_dir (keys %$src_sorted) {
    597 		my $src_cksums = read_cksums $src_dir;
    598 		next if !defined $src_cksums;
    599 		foreach my $src_file (@{$src_sorted->{$src_dir}}) {
    600 			my $src_path = "$src_dir/$src_file";
    601 
    602 			my $dst_path = $diff_name ? $dst : "$dst_dir/$src_file";
    603 			if (-d $dst_path && -d $src_path) {
    604 				warn "ERROR: Cannot copy directory to already existing directory\n";
    605 				next;
    606 			}
    607 			if (exists $SPECIAL_FILES{$src_file} || exists $SPECIAL_FILES{basename $dst_path}) {
    608 				warn "ERROR: Not copying special file\n";
    609 				next;
    610 			}
    611 			next if !$args->{"f"} && prompt_overwrite($dst_path);
    612 			my $options = $args->{"v"} ? "-av" : "-a";
    613 			next if system("cp", $options, "--", $src_path, $dst);
    614 
    615 			if (-d $src_path) {
    616 				$dirs_touched = 1;
    617 			} else {
    618 				$files_touched = 1;
    619 			}
    620 
    621 			if (exists $src_cksums->{$src_file}) {
    622 				if ($diff_name) {
    623 					$dst_cksums->{basename $dst} = $src_cksums->{$src_file};
    624 				} else {
    625 					$dst_cksums->{$src_file} = $src_cksums->{$src_file};
    626 				}
    627 			} else {
    628 				warn "WARNING: \"$src_path\" not in cksum or directory list\n";
    629 			}
    630 		}
    631 	}
    632 	write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched;
    633 }
    634 
    635 # move a file (or directory) from $src to $dst, prompting for confirmation if $dst already exists;
    636 # automatically appends the basename of $src to $dst if $dst is a directory
    637 sub move_file {
    638 	my ($src, $dst, $args) = @_;
    639 	if (exists $SPECIAL_FILES{basename $src} || exists $SPECIAL_FILES{basename $dst}) {
    640 		warn "ERROR: Not moving special file\n";
    641 		return 1;
    642 	}
    643 	if (-d $dst) {
    644 		$dst .= "/" . basename($src);
    645 	}
    646 	return 1 if !$args->{"f"} && prompt_overwrite($dst);
    647 	my $ret;
    648 	if ($args->{"v"}) {
    649 		$ret = system("mv", "-v", "--", $src, $dst);
    650 	} else {
    651 		$ret = system("mv", "--", $src, $dst);
    652 	}
    653 	return 1 if $ret;
    654 	if (-e $src) {
    655 		warn "ERROR: file could not be removed from source but will still be " .
    656 			"removed from checksum database\n";
    657 	}
    658 	return 0;
    659 }
    660 
    661 # move all files/directories in $src_files from $src_dir to $dst_dir ($src_files
    662 # only contains the basenames of the files), removing them from the checksum files
    663 # in $src_dir and adding them to $dst_cksums
    664 sub move_from_same_dir {
    665 	my ($src_dir, $src_files, $dst_cksums, $dst_dir, $args) = @_;
    666 	my $src_cksums = read_cksums $src_dir;
    667 	return if !defined $src_cksums;
    668 	my $files_touched = 0;
    669 	my $dirs_touched = 0;
    670 	foreach my $src_file (@$src_files) {
    671 		my $fullpath = "$src_dir/$src_file";
    672 		my $tmp_dirs_touched = 0;
    673 		my $tmp_files_touched = 0;
    674 		if (-d $fullpath) {
    675 			$tmp_dirs_touched = 1;
    676 		} else {
    677 			$tmp_files_touched = 1;
    678 		}
    679 
    680 		next if move_file($fullpath, $dst_dir, $args);
    681 
    682 		# need to be able to check if the path is a directory
    683 		# before actually moving it
    684 		$dirs_touched ||= $tmp_dirs_touched;
    685 		$files_touched ||= $tmp_files_touched;
    686 		if (exists $src_cksums->{$src_file}) {
    687 			$dst_cksums->{$src_file} = $src_cksums->{$src_file};
    688 			delete $src_cksums->{$src_file};
    689 		} else {
    690 			warn "WARNING: \"$src_dir/$src_file\" not in cksum or directory list.\n";
    691 		}
    692 	}
    693 	write_cksums $src_dir, $src_cksums, $files_touched, $dirs_touched;
    694 	return ($files_touched, $dirs_touched);
    695 }
    696 
    697 # rename a single file or directory from $src to $dst
    698 sub move_rename {
    699 	my ($src, $dst, $args) = @_;
    700 	my $src_dir = dirname $src;
    701 	my $dst_dir = dirname $dst;
    702 	my $src_file = basename $src;
    703 	my $dst_file = basename $dst;
    704 
    705 	my $src_cksums = read_cksums $src_dir;
    706 	return if !defined $src_cksums;
    707 	my $dst_cksums = {};
    708 	# if a file is simply being renamed in the same dir, the cksums
    709 	# should only be loaded and written once
    710 	if ($src_dir eq $dst_dir) {
    711 		%$dst_cksums = %$src_cksums;
    712 		delete $dst_cksums->{$src_file};
    713 	} else {
    714 		$dst_cksums = read_cksums $dst_dir;
    715 		return if !defined $dst_cksums;
    716 	}
    717 
    718 	my $files_touched = 0;
    719 	my $dirs_touched = 0;
    720 	if (-d $src) {
    721 		$dirs_touched = 1;
    722 	} else {
    723 		$files_touched = 1;
    724 	}
    725 
    726 	return if move_file($src, $dst, $args);
    727 
    728 	if (exists($src_cksums->{$src_file})) {
    729 		$dst_cksums->{$dst_file} = $src_cksums->{$src_file};
    730 		delete $src_cksums->{$src_file};
    731 	} else {
    732 		warn "WARNING: \"$src\" not in cksum or directory list.\n";
    733 	}
    734 	write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched;
    735 	if ($src_dir ne $dst_dir) {
    736 		write_cksums $src_dir, $src_cksums, $files_touched, $dirs_touched;
    737 	}
    738 }
    739 
    740 # move all files and directories in $src to $dst
    741 # - if $dst does not exist, $src is only allowed to contain one path, which is
    742 # renamed to $dst
    743 # - if $dst is a file, $src is only allowed to contain a single path (which
    744 # must be a file), which is renamed to $dst
    745 # - otherwise, all files and directories in $src are moved to $dst
    746 # $src: list of source paths
    747 # $dst: destination directory or file (in latter case only one src is allowed)
    748 sub move_files {
    749 	my ($src, $dst, $args) = @_;
    750 	if (!-d $dst && $#$src != 0) {
    751 		die "move: only one source argument allowed when destination is a file\n";
    752 	}
    753 	if (!-d $dst && !-d $src->[0]) {
    754 		move_rename $src->[0], $dst, $args;
    755 		return;
    756 	}
    757 	if (!-e $dst && -d $src->[0]) {
    758 		move_rename $src->[0], $dst, $args;
    759 		return;
    760 	}
    761 	if (-e $dst && !-d $dst && -d $src->[0]) {
    762 		die "move: can't move directory to file\n";
    763 	}
    764 	# Separate files by current dir so the cksum and dir files only need to be opened once
    765 	my $src_files = sort_by_dir(@$src);
    766 	my $dst_cksums = read_cksums $dst;
    767 	return if !defined $dst_cksums;
    768 	my $files_touched = 0;
    769 	my $dirs_touched = 0;
    770 	foreach my $src_dir (keys %$src_files) {
    771 		my ($tmp_files_touched, $tmp_dirs_touched) = move_from_same_dir $src_dir, $src_files->{$src_dir}, $dst_cksums, $dst, $args;
    772 		$files_touched ||= $tmp_files_touched;
    773 		$dirs_touched ||= $tmp_dirs_touched;
    774 	}
    775 	write_cksums $dst, $dst_cksums, $files_touched, $dirs_touched;
    776 }
    777 
    778 # remove a file or directory from the filesystem
    779 sub remove_file_dir {
    780 	my ($path, $args) = @_;
    781 	my $options = $args->{"f"} ? "-rf" : "-r";
    782 	if (system("rm", $options, "--", $path)) {
    783 		return 1;
    784 	}
    785 	if (-e $path) {
    786 		warn "ERROR: Unable to remove \"$path\" from filesystem but " .
    787 			"will still be removed from checksum database\n";
    788 	}
    789 	return 0;
    790 }
    791 
    792 # remove all files in one directory, updating the checksum files in the process
    793 # note: the files are only allowed to be basenames, i.e., they must be the
    794 # actual filenames present in the checksum files
    795 sub remove_from_same_dir {
    796 	my ($args, $dir, @files) = @_;
    797 	my $cksums = read_cksums $dir;
    798 	return if !defined $cksums;
    799 	my $dirs_touched = 0;
    800 	my $files_touched = 0;
    801 	foreach my $file (@files) {
    802 		if (exists $SPECIAL_FILES{$file}) {
    803 			warn "ERROR: not removing special file $file\n";
    804 			next;
    805 		}
    806 		my $fullpath = "$dir/$file";
    807 		if (!-e $fullpath) {
    808 			warn "\"$fullpath\": No such file or directory.\n";
    809 		}
    810 		next if remove_file_dir($fullpath, $args);
    811 		if (exists $cksums->{$file}) {
    812 			if (defined $cksums->{$file}) {
    813 				$files_touched = 1;
    814 			} else {
    815 				$dirs_touched = 1;
    816 			}
    817 			delete $cksums->{$file};
    818 		} else {
    819 			warn "WARNING: \"$file\" not in cksum or directory list.\n";
    820 		}
    821 	}
    822 	write_cksums $dir, $cksums, $files_touched, $dirs_touched;
    823 }
    824 
    825 # remove all given files and directories, updating the appropriate checksum
    826 # files in the process
    827 sub remove_files {
    828 	my $args = shift;
    829 	my $sorted_files = sort_by_dir(@_);
    830 	foreach my $dir (keys %$sorted_files) {
    831 		remove_from_same_dir($args, $dir, @{$sorted_files->{$dir}});
    832 	}
    833 }
    834 
    835 # create the given directories, initializing them with empty checksum files
    836 # note: does not work like "mkdir -p", i.e., the new directories have to
    837 # be located inside already existing directories
    838 sub make_dirs {
    839 	my @created_dirs;
    840 	foreach (@_) {
    841 		if (system("mkdir", "--", $_)) {
    842 			warn "ERROR creating directory $_\n";
    843 			next;
    844 		}
    845 		push(@created_dirs, $_);
    846 	}
    847 	# Separate files by current dir so the cksum and dir files only need to be opened once
    848 	my %dirs;
    849 	foreach my $dir (@created_dirs) {
    850 		write_cksums $dir, {}, 1, 1;
    851 		my $parent = dirname($dir);
    852 		if (!exists($dirs{$parent})) {
    853 			$dirs{$parent} = [];
    854 		}
    855 		push(@{$dirs{$parent}}, basename($dir));
    856 	}
    857 	foreach my $parent (keys %dirs) {
    858 		my $parent_dirs = read_file "$parent/$DIR_FILE", {};
    859 		next if !defined $parent_dirs;
    860 		foreach my $dir (@{$dirs{$parent}}) {
    861 			$parent_dirs->{$dir} = "";
    862 		}
    863 		write_file "$parent/$DIR_FILE", $parent_dirs;
    864 		write_special_cksums $parent, $DIR_FILE;
    865 	}
    866 }
    867 
    868 # extract all special lumia files from $src_dir to $dst_dir, recreating the
    869 # entire directory structure in the process
    870 sub extract {
    871 	my ($src_dir, $dst_dir, $args) = @_;
    872 	my $iter = make_lumia_iter 0, $src_dir;
    873 	my $options = $args->{"v"} ? "-av" : "-a";
    874 	while (my $dir = $iter->()) {
    875 		my $final_dir = abs2rel $dir, $src_dir;
    876 		my $fulldir = catfile $dst_dir, $final_dir;
    877 		system("mkdir", "-p", "--", $fulldir);
    878 		foreach my $file (keys %SPECIAL_FILES) {
    879 			my $filepath = catfile $dir, $file;
    880 			if (-e $filepath) {
    881 				system("cp", $options, "--", $filepath, catfile($fulldir, $file));
    882 			}
    883 		}
    884 	}
    885 }
    886 
    887 # update the checksums of the given files
    888 # ignores any directories given as arguments
    889 sub update {
    890 	my @files;
    891 	foreach (@_) {
    892 		if (-d $_) {
    893 			warn "Ignoring directory \"$_\"\n";
    894 		} else {
    895 			push @files, $_;
    896 		}
    897 	}
    898 	my $sorted_files = sort_by_dir @files;
    899 	foreach my $dir (keys %$sorted_files) {
    900 		my $cksums = read_cksum_file "$dir/$CKSUM_FILE", {};
    901 		next if !defined $cksums;
    902 		my $changed = 0;
    903 		foreach my $file (@{$sorted_files->{$dir}}) {
    904 			my $cksum_output = get_cksum "$dir/$file";
    905 			next if !defined $cksum_output;
    906 			$cksums->{$file} = $cksum_output;
    907 			$changed = 1;
    908 		}
    909 		if ($changed) {
    910 			write_cksum_file "$dir/$CKSUM_FILE", $cksums;
    911 			write_special_cksums $dir, $CKSUM_FILE;
    912 		}
    913 	}
    914 }
    915 
    916 sub update_special {
    917 	my $dir = shift;
    918 	write_special_cksums $dir, $CKSUM_FILE, $DIR_FILE;
    919 }
    920 
    921 my %args;
    922 Getopt::Long::Configure("bundling");
    923 GetOptions(\%args, "f|force", "q|quiet", "v|verbose", "h|help");
    924 
    925 pod2usage(-exitval => 0, -verbose => 2) if $args{"h"};
    926 pod2usage(-exitval => 1, -verbose => 1) if @ARGV < 1;
    927 
    928 my $cmd = shift;
    929 
    930 if ($cmd eq "mv") {
    931 	die "mv requires at least two arguments\n" if @ARGV < 2;
    932 	my @src = @ARGV[0..$#ARGV-1];
    933 	move_files \@src, $ARGV[-1], \%args;
    934 } elsif ($cmd eq "rm") {
    935 	die "rm requires at least one argument\n" if @ARGV < 1;
    936 	remove_files \%args, @ARGV;
    937 } elsif ($cmd eq "addnew") {
    938 	my $dir = @ARGV ? $ARGV[0] : ".";
    939 	check_add_new_files $dir, \%args;
    940 } elsif ($cmd eq "checknew") {
    941 	my $dir = @ARGV ? $ARGV[0] : ".";
    942 	check_new_files $dir;
    943 } elsif ($cmd eq "checkold") {
    944 	my $dir = @ARGV ? $ARGV[0] : ".";
    945 	check_old_files $dir;
    946 } elsif ($cmd eq "rmold") {
    947 	my $dir = @ARGV ? $ARGV[0] : ".";
    948 	remove_old_files $dir, \%args;
    949 } elsif ($cmd eq "check") {
    950 	if (@ARGV < 1) {
    951 		check_files \%args, ".";
    952 	} else {
    953 		check_files \%args, @ARGV;
    954 	}
    955 } elsif ($cmd eq "clean") {
    956 	my $dir = @ARGV ? $ARGV[0] : ".";
    957 	clean_files $dir, \%args;
    958 } elsif ($cmd eq "extract") {
    959 	my $src_dir = ".";
    960 	my $dst_dir;
    961 	if (@ARGV == 2) {
    962 		$src_dir = $ARGV[0];
    963 		$dst_dir = $ARGV[1];
    964 	} elsif (@ARGV == 1) {
    965 		$dst_dir = $ARGV[0];	
    966 	} else {
    967 		die "Invalid number of arguments\n";
    968 	}
    969 	if (!-d $src_dir) {
    970 		die "ERROR: Directory \"$src_dir\" does not exist.\n";
    971 	}
    972 	if (!-d $dst_dir) {
    973 		die "ERROR: Directory \"$dst_dir\" does not exist.\n";
    974 	}
    975 	extract $src_dir, $dst_dir;
    976 } elsif ($cmd eq "cp") {
    977 	die "cp requires at least two arguments\n" if @ARGV < 2;
    978 	my @src = @ARGV[0..$#ARGV-1];
    979 	copy_files \@src, $ARGV[-1], \%args;
    980 } elsif ($cmd eq "mkdir") {
    981 	die "mkdir requires at least one argument\n" if @ARGV < 1;
    982 	make_dirs @ARGV;
    983 } elsif ($cmd eq "update") {
    984 	die "update requires at least one argument\n" if @ARGV < 1;
    985 	update @ARGV;
    986 } elsif ($cmd eq "updatespecial") {
    987 	die "Invalid number of arguments\n" if @ARGV > 1;
    988 	my $dir = @ARGV ? $ARGV[0] : ".";
    989 	update_special $dir;
    990 } else {
    991 	pod2usage(-exitval => 1, -verbose => 1);
    992 }
    993 
    994 __END__
    995 
    996 =head1 NAME
    997 
    998 lumia - Manage checksums on a filesystem
    999 
   1000 =head1 SYNOPSIS
   1001 
   1002 B<lumia> command [-hqfv] arguments
   1003 
   1004 =head1 DESCRIPTION
   1005 
   1006 lumia is meant for managing checksums of files in order to prevent bitrot.
   1007 It does this by storing several special files in each directory to keep track
   1008 of the checksums:
   1009 
   1010 =over 8
   1011 
   1012 =item B<.lumidify_archive_cksums>
   1013 
   1014 Contains the checksums of all files in the directory.
   1015 
   1016 =item B<.lumidify_archive_dirs>
   1017 
   1018 Contains a list of all directories in the directory.
   1019 
   1020 =item B<.lumidify_archive_cksums.cksum>
   1021 
   1022 Contains the checksums of B<.lumidify_archive_cksums> and B<.lumidify_archive_dirs>
   1023 just because I'm paranoid.
   1024 
   1025 =item B<.lumidify_archive_ignore>
   1026 
   1027 Contains a list of files and directories that should be ignored by lumia.
   1028 Note that this is only read and never written to, unless the command B<clean>
   1029 is used. It is, however, still copied over by the B<extract> command.
   1030 
   1031 =back
   1032 
   1033 When the documentation for the commands talks about the "checksum database",
   1034 it simply means these files.
   1035 
   1036 All file/directory names are enclosed in quotes, with any backslashes or quotes
   1037 inside the name escaped with another backslash. The names are allowed to have
   1038 newlines in them.
   1039 
   1040 The list files only contain a list of filenames, with a newline between the
   1041 closing quote of one name and the opening quote of the next one.
   1042 
   1043 The checksum files additionally contain the output of the checksum program
   1044 used and a space before the starting quote of the filename.
   1045 
   1046 =head1 OPTIONS
   1047 
   1048 =over 8
   1049 
   1050 =item B<-h>, B<--help>
   1051 
   1052 Show the full documentation.
   1053 
   1054 =item B<-q>, B<--quiet>
   1055 
   1056 Only output errors.
   1057 
   1058 =item B<-f>, B<--force>
   1059 
   1060 Overwrite files without prompting for confirmation.
   1061 
   1062 =item B<-v>, B<--verbose>
   1063 
   1064 Print each file that is processed by the command.
   1065 
   1066 =back
   1067 
   1068 See the full documentation for details on which commands support which options
   1069 and what they do.
   1070 
   1071 It does not matter if the options are written before or after the command.
   1072 
   1073 If C<--> is written anywhere on the command line, option parsing is stopped,
   1074 so that files starting with a hyphen can still be specified.
   1075 
   1076 Note that C<-q> and C<-v> aren't exactly opposites - C<-q> applies to commands
   1077 like B<check>, where it suppresses printing of the individual files, while
   1078 C<-v> applies to commands like B<cp>, where it is just passed on to the system
   1079 command called in the background.
   1080 
   1081 Note further that this is very inconsistent, like the rest of the program, but
   1082 the author has made too many bad decisions to rectify that problem at the moment.
   1083 
   1084 =head1 COMMANDS
   1085 
   1086 Note that some commands support multiple files/directories as arguments and others,
   1087 for which it would make just as much sense, don't. That's just the way it is.
   1088 
   1089 =over 8
   1090 
   1091 =item B<addnew> [-q] [directory]
   1092 
   1093 Walks through B<directory>, adding all new files to the checksum database.
   1094 B<directory> defaults to the current directory.
   1095 
   1096 C<-q> suppresses the printing of each file or directory as it is added.
   1097 
   1098 =item B<checknew> [directory]
   1099 
   1100 Walks through B<directory>, printing all files that aren't part of the checksum
   1101 database. B<directory> defaults to the current directory.
   1102 
   1103 =item B<checkold> [directory]
   1104 
   1105 Prints all files in the checksum database that do not exist on the filesystem anymore.
   1106 B<directory> defaults to the current directory.
   1107 
   1108 =item B<rmold> [-q] [directory]
   1109 
   1110 Removes all files found by B<checkold> from the database. B<directory> defaults to
   1111 the current directory.
   1112 
   1113 C<-q> suppresses the printing of each file as it is removed.
   1114 
   1115 =item B<check> [-q] file/directory ...
   1116 
   1117 Verifies the checksums of all files given, recursing through any directories. If no
   1118 files or directories are given, the current directory is used.
   1119 
   1120 Note that the checksum database in the corresponding directory will be read again for
   1121 every file given on the command line, even if 1000 files in the same directory are given.
   1122 This problem does not occur when recursing through directories, so it is best to only
   1123 give files directly when checking a few. This problem wouldn't be too difficult to
   1124 fix, but, frankly, I'm too lazy, especially since I only added the feature to check
   1125 files individually as a convenience when I want to quickly check a single file in a
   1126 large directory.
   1127 
   1128 To explain why it is this way: The directory recursion is done using an iterator, which
   1129 has the directories pushed onto its queue in the beginning. The iterator only returns
   1130 directories, which are then checked all in one go, but this means that files given on
   1131 the command line need to be handled specially.
   1132 
   1133 C<-q> suppresses the printing of all good checksums but still allows a message to
   1134 be printed when a checksum failed.
   1135 
   1136 =item B<clean> [-q] [directory]
   1137 
   1138 Removes all lumia special files used to store the checksum database from B<directory>
   1139 recursively. B<directory> defaults to the current directory.
   1140 
   1141 Note that this recurses through the entire directory tree, not just the part that is
   1142 actually linked together by the checksum database.
   1143 
   1144 Warning: This just blindly removes all files with one of the special lumia names,
   1145 even if they weren't actually created by lumia.
   1146 
   1147 C<-q> suppresses the printing of each file as it is deleted.
   1148 
   1149 =item B<extract> [-v] [source] destination
   1150 
   1151 Recreates the entire directory structure from B<source> in B<destination>, but only
   1152 copies the special files used to store the checksum database. B<source> defaults to
   1153 the current directory.
   1154 
   1155 C<-v> prints each file as it is copied.
   1156 
   1157 Note that this overwrites files in the destination directory without confirmation.
   1158 
   1159 =item B<mkdir> directory ...
   1160 
   1161 Creates the given directories, initializing them with empty checksum database files.
   1162 
   1163 =item B<update> file ...
   1164 
   1165 Recalculates the checksums for the given files and replaces them in the database.
   1166 
   1167 Note: Directories given as arguments are ignored.
   1168 
   1169 This is mainly meant to quickly "touch" a file after it was modified (e.g. a
   1170 notes file that is occasionally updated).
   1171 
   1172 =item B<updatespecial> [directory]
   1173 
   1174 Recalculates the checksums for the special files C<.lumidify_archive_dirs> and
   1175 C<.lumidify_archive_cksums> and writes them to C<.lumidify_archive_cksums.cksum>.
   1176 B<directory> defaults to the current directory.
   1177 
   1178 This is only meant to be used if, for some reason, the checksum files had to
   1179 be edited manually and thus don't match the checksums in C<.lumidify_archive_cksums.cksum>
   1180 anymore.
   1181 
   1182 =item B<rm> [-f] file ...
   1183 
   1184 Removes the given files and directories recursively from the filesystem and
   1185 checksum database. The following caveats apply:
   1186 
   1187 If any actual errors occur while deleting the file/directory (i.e. the system
   1188 command C<rm> returns a non-zero exit value), the checksum or directory B<is
   1189 left in the database>. If the system C<rm> does not return a non-zero exit value,
   1190 but the file/directory still exists afterwards (e.g. there was a permission
   1191 error and the user answered "n" when prompted), a warning message is printed,
   1192 but the files B<are removed from the database> (if the database can be
   1193 written to).
   1194 
   1195 It is an error if there are no checksum database files in the directory
   1196 of a file named on the command line.
   1197 
   1198 C<-f> is passed through to the system C<rm> command.
   1199 
   1200 =item B<cp> [-vf] source target
   1201 
   1202 =item B<cp> [-vf] source ... directory
   1203 
   1204 Copies the given source files, updating the checksum database in the process.
   1205 
   1206 If the last argument is a file, there must be only one source argument, also a file,
   1207 which is then copied to the target.
   1208 
   1209 If the last argument is a directory, all source arguments are copied into it.
   1210 
   1211 It is an error if a source or destination directory does not contain any
   1212 checksum database files.
   1213 
   1214 B<cp> will issue a warning and skip to the next argument if it is asked to
   1215 merge a directory with an already existing directory. For instance, attempting
   1216 to run C<cp dir1 dir2>, where C<dir2> already contains a directory named
   1217 C<dir1>, will result in an error. This may change in the future, when the
   1218 program is modified to recursively copy the files manually, instead of simply
   1219 calling the system C<cp> on each of the arguments. If this was supported in
   1220 the current version, none of the checksums inside that directory would be
   1221 updated, so it wouldn't be very useful.
   1222 
   1223 C<-v> is passed through to the system C<cp> command.
   1224 
   1225 C<-f> silently overwrites files without prompting the user, much like the
   1226 C<-f> option in the system C<cp> command. This is handled manually by the
   1227 program, though, in order to actually determine what the user chose. See
   1228 also the caveat mentioned above.
   1229 
   1230 =item B<mv> [-f] source target
   1231 
   1232 =item B<mv> [-f] source ... directory
   1233 
   1234 Moves the given source files, updating the checksum database in the process.
   1235 
   1236 If the last argument is a file or does not exist, there must be only one source
   1237 argument, which is renamed to the target name.
   1238 
   1239 If the last argument is an existing directory, all source arguments are moved
   1240 into it.
   1241 
   1242 It is an error if a source or destination directory does not contain any
   1243 checksum database files.
   1244 
   1245 B<mv> behaves the same as B<rm> with regards to checking if the source file
   1246 is still present after the operation and other error handling.
   1247 
   1248 C<-f> is handled in the same manner as with B<cp>.
   1249 
   1250 =back
   1251 
   1252 =head1 MOTIVATION
   1253 
   1254 There are already several programs that can be used to check for bitrot,
   1255 as listed in L</"SEE ALSO">. However, all programs I tried either were
   1256 much too complicated for my taste or just did everything behind my back.
   1257 I wanted a simple tool that did exactly what I told it to and also allowed
   1258 me to keep the old checksums when reorganizing files, in order to avoid
   1259 regenerating the checksums from corrupt files. Since I couldn't find those
   1260 features in any program I tried, I wrote my own.
   1261 
   1262 =head1 DESIGN DECISIONS
   1263 
   1264 It may strike some readers as a peculiar idea to save the checksum files in
   1265 I<every single directory>, but this choice was made after much deliberation.
   1266 The other option I could think of was to have one big database, but that
   1267 would have made all commands much more difficult to implement and additionally
   1268 necessitated opening the entire database for every operation. With individual
   1269 files in each directory, operations like B<cp> become quite trivial (ignoring
   1270 all the edge cases) since only the toplevel checksums need to be copied to
   1271 the new destination, and any subdirectories already contain the checksums.
   1272 
   1273 This method is not without its drawbacks, however. The most glaring problem
   1274 I have found is that there is no way to store the checksums of read-only
   1275 directories or any special directories that cannot be littered with the
   1276 checksum files because that would clash with other software. Despite these
   1277 drawbacks, however, I decided to stick with it because it works for almost
   1278 all cases and doesn't have any of the serious drawbacks that other options
   1279 would have had.
   1280 
   1281 The names of the special files were chosen to be ".lumidify_archive*" not
   1282 out of vanity, but mainly because I couldn't think of any regular files
   1283 with those names, making them a good choice to avoid clashes.
   1284 
   1285 The name of the program, C<lumia> (for "lumidify archive"), was similarly
   1286 chosen because it did not clash with any programs installed on my system and
   1287 thus allowed for easy tab-completion.
   1288 
   1289 =head1 HASH ALGORITHMS
   1290 
   1291 By default, the simple cksum algorithm is used to get the checksums. This
   1292 is not very secure, but the main purpose of the program is to prevent
   1293 bitrot, for which cksum should be sufficient, especially since it is much
   1294 faster than other algorithms.
   1295 
   1296 There is currently no convenient way to change the algorithm other than
   1297 changing the $CKSUM_CMD and $CKSUM_NUMFIELDS variables at the top of
   1298 lumia. $CKSUM_CMD must be the command that returns the checksum
   1299 when it is given a file, and $CKSUM_NUMFIELDS specifies the number of
   1300 space-separated fields the checksum consists of. This has to be specified
   1301 in order to determine where the checksum ends and the filename begins in
   1302 the output. This would be redundant if all implementations of cksum
   1303 supported '-q' for outputting only the checksum, but that only seems to
   1304 be supported by some implementations.
   1305 
   1306 =head1 USAGE SCENARIOS
   1307 
   1308 =over 8
   1309 
   1310 =item B<Security auditing>
   1311 
   1312 This program is B<NOT> designed to provide any security auditing, as should
   1313 be clear from the fact that the checksums are stored right in the same
   1314 directory as the files. See mtree(8) for that.
   1315 
   1316 If you want to, however, you could set $CKSUM_CMD to a secure hash (not cksum)
   1317 and B<extract> the checksums to a separate directory, which you keep in a
   1318 safe place. You could then use the regular C<cp> command to simply replace
   1319 all the checksums with the ones from your backup, in case an attacker modified
   1320 the checksum database in the directory with the actual files you're trying to
   1321 protect. I don't know if there would be any point in doing that, though.
   1322 
   1323 =item B<Managing archives>
   1324 
   1325 This is the purpose I wrote the program for.
   1326 
   1327 You can simply initialize your archive directory with the B<addnew> command.
   1328 Whenever you add new files, just run B<addnew> again. If you want to reorganize
   1329 the archive, you can use the limited commands available.
   1330 
   1331 I usually just use rsync(1) to copy the entire archive directory over to other
   1332 backup drives and then use the B<check> command again on the new drive.
   1333 
   1334 I also have checksums for the main data directory on my computer (except for
   1335 things like git repositories, which I don't want littered with the database
   1336 files). Here, I use the B<update> command for files that I edit more often
   1337 and occasionally run B<check> on the entire directory.
   1338 
   1339 Since the database files are written in each directory, you can run the
   1340 B<addnew> command in any subdirectory when you've added new files there.
   1341 
   1342 =back
   1343 
   1344 =head1 PERFORMANCE
   1345 
   1346 Due to the extensive use of iterators and the author's bad life choices,
   1347 some functions, such as B<addnew> and B<check>, run more slowly than they
   1348 would if they were programmed more efficiently, especially on many small
   1349 files and folders. Too bad.
   1350 
   1351 =head1 PORTABILITY
   1352 
   1353 This program was written on OpenBSD. It will probably work on most other
   1354 reasonably POSIX-Compliant systems, although I cannot guarantee anything.
   1355 $CKSUM_CMD may need to be modified at the top of lumia. The file
   1356 operation commands are called directly with system(), so those need to
   1357 be available.
   1358 
   1359 It will most certainly not work on Windows, but that shouldn't be a
   1360 problem for anyone important.
   1361 
   1362 =head1 BUGS
   1363 
   1364 All system commands (unless I forgot some) are called with "--" before
   1365 listing the actual files, so files beginning with hyphens should be
   1366 supported. I have tested the commands with filenames starting with spaces
   1367 and hyphens and also containing newlines, but there may very well be issues
   1368 still. Please notify me if you find any filenames that do not work. Handling
   1369 filenames properly is difficult.
   1370 
   1371 There are probably many other edge cases, especially in the B<mv>, B<cp>,
   1372 and B<rm> commands. Please notify me if you find an issue.
   1373 
   1374 Operations on filenames containing newlines may cause Perl to print a warning
   1375 "Unsuccessful stat on filename containing newline" even though nothing is
   1376 wrong since (as described in B<mv> and B<rm>) existence of the file is
   1377 checked afterwards. I didn't feel like disabling warnings, and no normal
   1378 person should be working with filenames containing newlines anyways, so that's
   1379 the way it is.
   1380 
   1381 =head1 EXIT STATUS
   1382 
   1383 Always 0, unless the arguments given were invalid. We don't do errors around here.
   1384 
   1385 On a more serious note - I should probably change that at some point.
   1386 For the time being, if you want to run B<check> in a script, you can test
   1387 the output printed when the C<-q> option is used, since this won't output
   1388 anything if there are no errors. Do note, though, that actual errors (file not
   1389 found, etc.) are printed to STDERR, while incorrect checksums are printed
   1390 to STDOUT.
   1391 
   1392 =head1 SEE ALSO
   1393 
   1394 par2(1), mtree(8), aide(1), bitrot(no man page)
   1395 
   1396 =head1 LICENSE
   1397 
   1398 Copyright (c) 2019, 2020, 2021 lumidify <nobody[at]lumidify.org>
   1399 
   1400 Permission to use, copy, modify, and/or distribute this software for any
   1401 purpose with or without fee is hereby granted, provided that the above
   1402 copyright notice and this permission notice appear in all copies.
   1403 
   1404 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   1405 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
   1406 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
   1407 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
   1408 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
   1409 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
   1410 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
   1411 
   1412 =cut