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