commit f60d1fa300a9def9bed31e38e9c0025676705efd
parent 15d5db13de4e74e63ecf44a0d687712124a6bca8
Author: lumidify <nobody@lumidify.org>
Date:   Mon, 23 Mar 2020 08:40:19 +0100
Start rewriting in a cleaner fashion
Diffstat:
| M | lumia.pl |  |  | 881 | ++++++++++++++++++++++++++++++++++--------------------------------------------- | 
1 file changed, 378 insertions(+), 503 deletions(-)
diff --git a/lumia.pl b/lumia.pl
@@ -34,74 +34,95 @@ use Data::Dumper;
 use Scalar::Util qw(looks_like_number);
 use Getopt::Long;
 
-sub make_dir_iter {
-	my @queue = @_;
+sub escape_filename {
+	my $file = shift;
+	$file =~ s/\\/\\\\/g;
+	$file =~ s/"/\\"/g;
+	return $file;
+}
+sub make_file_iter {
+	my ($file_func, $dir_func, @queue) = @_;
 	return sub {
 		while (@queue) {
 			my $file = shift @queue;
-			if (!-d $file) {
-				warn "WARNING: \"$file\" is not directory!";
-				next;
-			}
-			my $dh;
-			if (!opendir $dh, $file) {
-				warn "WARNING: Unable to open directory \"$file\"!";
-				next;
+			if (-d $file) {
+				my $new_files = $dir_func->($file);
+				next if !defined $new_files;
+				push @queue, @$new_files;
 			}
-			my @newdirs = grep {-d "$file/$_" && $_ ne "." && $_ ne ".."} readdir $dh;
-			push @queue, map "$file/$_", @newdirs;
-			closedir $dh;
-			return $file;
+			return $file if $file_func->($file);
 		}
 		return;
 	};
 }
 
+sub make_file_iter_basic {
+	my ($file_func, @files) = @_;
+	make_file_iter $file_func, sub {
+		my $dh;
+		if (!opendir $dh, $_[0]) {
+			warn "WARNING: Unable to open directory \"$_[0]\"!";
+			return [];
+		}
+		my @new_files = map "$_[0]/$_", grep {$_ ne "." && $_ ne ".."} readdir $dh;
+		closedir $dh;
+		return \@new_files;
+	}, @files;
+}
+
+sub make_dir_iter {
+	make_file_iter_basic sub {-d $_[0]}, @_;
+}
+
+sub make_lumia_iter {
+	make_file_iter sub {1}, sub {
+		my $path = "$_[0]/.lumidify_archive_dirs";
+		return [] if !-f $path;
+		my $dirs = read_file($path);
+		return if !defined $dirs;
+		my @new_dirs;
+		foreach my $dir (keys %$dirs) {
+			my $dir_path = "$_[0]/$dir";
+			if (!-d $dir_path) {
+				warn "ERROR: Directory \"$dir_path\" does not exist or is not directory.\n");
+			} else {
+				push @new_dirs, $dir_path;
+			}
+		}
+		return \@new_dirs;
+	}, @_;
+}
+
 my $CKSUM_CMD = 'cksum -q';
-my $CKSUM_CHECK_CMD = 'cksum -c';
-my $CKSUM_CHECK_SINGLE_CMD = 'cksum -q';
+my %SPECIAL_FILES = (
+	".lumidify_archive_cksums" => 1,
+	".lumidify_archive_cksums.cksum" => 1,
+	".lumidify_archive_ignore" => 1,
+	".lumidify_archive_dirs" => 1
+);
 
 sub clean_files {
-	my @files = (
-		".lumidify_archive_cksums",
-		".lumidify_archive_cksums.cksum",
-		".lumidify_archive_ignore",
-		".lumidify_archive_dirs"
-	);
-	for my $file (@files) {
-		if (-e $file) {
-			my $dir = getcwd();
-			unlink($file, {safe => 1}) or return "ERROR: can't remove \"$dir/$file\": $!";
-			print("Deleted \"$dir/$file\"\n");
-		}
-	}
-	opendir(my $dh, ".") or die("ERROR: Unable to list files in \"" . getcwd() . "\"\n");
-	my @dirs;
-	my $file;
-	while ($file = readdir($dh)) {
-		next if ($file =~ /\A\.\.?\z/);
-		# Just so these files aren't all left open while recursing
-		if (-d $file) {
-			push(@dirs, $file);
-		}
-	}
-	closedir($dh);
-	my $dir = getcwd();
-	foreach (@dirs) {
-		chdir($_);
-		clean_files();
-		chdir($dir);
+	my $dir = shift;
+	my $match_lumia_files = sub {
+		exists $SPECIAL_FILES{basename $_[0]};
+	};
+	my $iter = make_file_iter $match_lumia_files, $dir;
+	while (my $file = $iter->()) {
+		if (!unlink $file) {
+			warn "WARNING: Unable to remove file \"$file\"!";
+		} else {
+			print "Deleted \"$file\"";
+		}
 	}
 }
 
 sub read_file {
 	my ($file, $is_cksums_file) = @_;
-	my $path = catfile(getcwd(), $file);
 	my %cksums;
 	my $fh;
 	if (!open($fh, "<", $file)) {
-		print(STDERR "ERROR: unable to open file \"$path\": $!\n");
-		return;
+		warn "ERROR: unable to open file \"$file\": $!\n";
+		return undef;
 	}
 	my $in_fn = 0;
 	my $cur_cksum;
@@ -112,7 +133,7 @@ sub read_file {
 		if ($is_cksums_file && !$in_fn) {
 			my @fields = split(/ /, $_, 3);
 			if ($#fields != 2) {
-				print(STDERR "ERROR: malformed line \"$_\" in file \"$path\"\n");
+				warn "ERROR: Malformed line \"$_\" in file \"$file\"\n");
 				next;
 			}
 			$cur_cksum = join(" ", @fields[0,1]);
@@ -142,375 +163,236 @@ sub read_file {
 		}
 	}
 	if ($in_fn) {
-		print(STDERR "ERROR: unterminated filename in file \"$path\"\n");
+		warn "ERROR: Unterminated filename in file \"$path\"\n";
+		return undef;
 	}
 	return \%cksums;
 }
 
+sub read_cksums {
+	my $dir = shift;
+	my $file_cksums = read_file "$dir/.lumidify_archive_cksums", 1;
+	my $dir_list = read_file "$dir/.lumidify_archive_dirs";
+	return undef if !defined $file_cksums || !defined $dir_list;
+	my %cksums = (%$file_cksums, %$dir_list);
+	return \%cksums;
+}
+
+sub get_cksum {
+	my $path = shift;
+	my $path_esc = shell_quote $path;
+	$cksum_output = `$CKSUM_CMD -- $path_esc 2>&1`;
+	if ($?) {
+		warn "ERROR getting cksum for file \"$path\":\n$cksum_output\n";
+		return undef;
+	}
+	chomp $cksum_output;
+	return $cksum_output;
+}
+
 # This would be much cleaner with cksums's -c option, but that
 # doesn't seem to work with files beginning with whitespace
 sub check_cksums {
-	my $cksum_file = shift;
-	my $cksums = read_file($cksum_file, 1);
+	my ($dir, $cksum_file) = @_;
+	my $cksums = read_file "$dir/$cksum_file", 1;
+	return if !defined $cksums;
 	foreach my $file (keys %$cksums) {
-		my $file_esc = shell_quote($file);
-		my $output = `$CKSUM_CHECK_SINGLE_CMD -- $file_esc 2>&1`;
-		my $path = catfile(getcwd(), $file);
-		if ($?) {
-			print(STDERR "ERROR getting cksum for file \"$path\":\n$output\n");
-			next;
-		}
-		chomp($output);
+		my $path = catfile($dir, $file);
+		my $output = get_cksum $path;
+		next if !defined $output;
 		print(($output eq $cksums->{$file} ? "OK" : "FAILED") . "  $path\n");
 	}
-	return;
-	my $fullpath = catfile(getcwd(), $cksum_file);
-	my $fh;
-	if (!open($fh, "<", $cksum_file)) {
-		print("ERROR: unable to open cksum file \"$fullpath\": $!\n");
-		return;
-	}
-	foreach (<$fh>) {
-		chomp;
-		next if !$_;
-		my @fields = split(/ /, $_, 3);
-		if ($#fields != 2) {
-			print("ERROR: malformed line \"$_\" in file \"$fullpath\"\n");
-			next;
-		}
-		my $cksum = join(" ", @fields[0,1]);
-		my $file = shell_quote($fields[2]);
-		# '--' needed so it works on files with leading '-'
-		my $output = `$CKSUM_CHECK_SINGLE_CMD -- $file`;
-		my $path = catfile(getcwd(), $fields[2]);
-		if ($?) {
-			print("ERROR getting cksum for file \"$path\":\n$output\n");
-			next;
-		}
-		chomp($output);
-		print(($output eq $cksum ? "OK" : "FAILED") . "  $path\n");
-	}
-	close($fh);
 }
 
 sub check_files {
-	check_cksums(".lumidify_archive_cksums.cksum");
-	check_cksums(".lumidify_archive_cksums");
-	#my $output = `$CKSUM_CHECK_CMD .lumidify_archive_cksums.cksum`;
-	#if ($?) {
-	#	print("ERROR in directory \"" . getcwd() . "\"\n");
-	#}
-	# It would be better to do this properly with cksum printing its output
-	# directly, but I couldn't get system() to work with quitting if SIGINT
-	# is received (yes, I followed the instructions in perldoc -f system)
-	#print($output);
-	#if (-s ".lumidify_archive_cksums") {
-	#	$output = `$CKSUM_CHECK_CMD .lumidify_archive_cksums`;
-	#	if ($?) {
-	#		print("ERROR in directory \"" . getcwd() . "\"\n");
-	#	}
-	#	print($output);
-	#}
-	return if (!-f ".lumidify_archive_dirs");
-	my $dirs = read_file(".lumidify_archive_dirs");
-	foreach my $dir (keys %$dirs) {
-		my $cwd = getcwd();
-		if (!-d $dir) {
-			print("ERROR: directory \"$cwd/$dir\" does not exist anymore.\n");
-			next;
-		}
-		chdir($dir);
-		check_files();
-		chdir($cwd);
-	}
-	return;
-	open(my $fh, "<", ".lumidify_archive_dirs") or die("ERROR: Unable to open \"" . getcwd() . "/.lumidify_archive_dirs\"\n");
-	my @dirs;
-	foreach (<$fh>) {
-		chomp;
-		next if (!$_);
-		# Just so these files aren't all left open while recursing
-		push(@dirs, $_);
-	}
-	close($fh);
-	foreach (@dirs) {
-		my $dir = getcwd();
-		if (!-d $_) {
-			print("ERROR: directory \"$dir/$_\" does not exist anymore.\n");
-			next;
-		}
-		chdir($_);
-		check_files();
-		chdir($dir);
+	my $top_dir = shift;
+	my $iter = make_lumia_iter $dir;
+	while (my $dir = $iter->()) {
+		check_cksums $dir, ".lumidify_archive_cksums.cksum";
+		check_cksums $dir, ".lumidify_archive_cksums";
 	}
 }
 
-sub check_new_files_recurse {
-	my $dir = shift;
-	my $cur_hash = shift;
-	my $cur_cksum_hash = shift;
-	#my %ignore;
-	my $ignore = {};
-	if (-f catfile($dir, '.lumidify_archive_ignore')) {
-		$ignore = read_file(catfile($dir, ".lumidify_archive_ignore"));
-	}
-=pod
-		open(my $fh, "<", catfile($dir, '.lumidify_archive_ignore')) or die("Can't open $dir/.lumidify_archive_ignore: $!\n");
-		foreach (<$fh>) {
-			chomp;
-			$ignore{$_} = undef;
-		}
-		close($fh);
-	}
-=cut
-	opendir(my $dh, $dir) or die("Can't open $dir: $!\n");
-	my $file;
-	my $fullpath;
-	while ($file = readdir($dh)) {
-		next if ($file =~ /\A\.\.?\z/);
-		next if (exists($ignore->{$file}));
-		next if ($file eq '.lumidify_archive_cksums' ||
-			$file eq '.lumidify_archive_cksums.cksum' ||
-			$file eq '.lumidify_archive_dirs' ||
-			$file eq '.lumidify_archive_ignore');
-		$fullpath = catfile($dir, $file);
-		if (-d $fullpath) {
-			$cur_hash->{$file} = {};
-			if (!defined($cur_cksum_hash) || !exists($cur_cksum_hash->{$file})) {
-				check_new_files_recurse($fullpath, $cur_hash->{$file}, undef);
-			} else {
-				check_new_files_recurse($fullpath, $cur_hash->{$file}, $cur_cksum_hash->{$file});
-			}
-		} else {
-			if (!defined($cur_cksum_hash) || !exists($cur_cksum_hash->{$file})) {
-				$cur_hash->{$file} = undef;
-			}
-		}
-	}
-	closedir($dh);
+sub write_special_cksums {
+	my ($dir, @files) = @_;
+	my $cksum_file = "$dir/.lumidify_archive_cksums.cksum";
+	my $cksums = read_file $cksum_file, 1;
+	foreach my $file (@files) {
+		my $cksum_output = get_cksum "$dir/$file";
+		next if (!defined $cksum_output);
+		$cksums->{$file} = $cksum_output;
+	}
+	write_file($cksum_file, $cksums, 1);
 }
 
 sub check_new_files {
-	my $dir = shift;
-	my $cksum_tree = shift;
-	my %new_hash;
-	check_new_files_recurse($dir, \%new_hash, $cksum_tree);
-	return \%new_hash;
+	my ($dir, $file_func, $before_dir_func, $after_dir_func) = @_;
+	my $iter = make_file_iter sub {-d $_[0]}, sub {
+		my $dh;
+		if (!opendir $dh, $_[0]) {
+			warn "ERROR: Unable to open directory \"$_[0]\"!";
+			return undef;
+		}
+		my $ignore = read_file "$_[0]/.lumidify_archive_ignore" // {};
+		my $lumia_dirs = read_file "$_[0]/.lumidify_archive_dirs" // {};
+		my $lumia_files = read_file "$_[0]/.lumidify_archive_cksums", 1 // {};
+		my @dirs;
+		my $found = 0;
+		while (my $file = readdir $dh) {
+			next if $file eq "." || $file eq "..";
+			next if exists $ignore->{$file} || exists $SPECIAL_FILES{$file};
+			if (!exists $lumia_dirs->{$file} && !exists $lumia_files->{$file}) {
+				if (!found && defined $before_dir_func) {
+					$before_dir_func->($_[0]);
+				}
+				if (defined $file_func) {
+					$file_func->($_[0], $file);
+				} else {
+					print "$_[0]/$file";
+				}
+				$found = 1;
+			}
+			push @dirs, "$_[0]/$file" if -d "$_[0]/$file";
+		}
+		closedir $fh;
+		if (found && defined $after_dir_func) {
+			$after_dir_func->($_[0]);
+		}
+		return \@dirs;
+	}, $dir;
+	# Is this a horrible hack? I dunno, but it sure is sweet...
+	while ($iter->()) {}
 }
 
-sub add_new_files_recurse {
-	my $dir = shift;
-	my $cur_hash = shift;
-	my $cur_cksum_hash = shift;
-	my $fullpath;
-	my $cksum_output;
-	foreach my $file (keys(%$cur_hash)) {
-		$fullpath = catfile($dir, $file);
+sub check_add_new_files {
+	my $top_dir = shift;
+	my $changed_dirs = 0;
+	my $changed_files = 0;
+	check_new_files $top_dir, sub {
+		my ($dir, $file) = @_;
+		my $fullpath = "$dir/$file";
 		if (-d $fullpath) {
-			if (!exists($cur_cksum_hash->{$file})) {
-				$cur_cksum_hash->{$file} = {};
+			my $dir_file = "$dir/.lumidify_archive_dirs";
+			my $fh;
+			if (!open $fh, ">>", $dir_file) {
+				warn "ERROR: Unable to append to file \"$dir_file\"!";
+				return;
 			}
-			add_new_files_recurse($fullpath, $cur_hash->{$file}, $cur_cksum_hash->{$file});
+			print $fh, "\"" . escape_filename $file . "\"\n";
+			close $fh;
+			$changed_dirs = 1;
 		} else {
-			my $path_esc = shell_quote($fullpath);
-			$cksum_output = `$CKSUM_CMD -- $path_esc`;
-			chomp($cksum_output);
-			# TODO: check for error
-			$cur_cksum_hash->{$file} = $cksum_output;
-			print("Added checksum: $cksum_output $fullpath\n");
+			my $cksum_output = get_cksum $fullpath;
+			return if !defined $cksum_output;
+			my $cksum_file = "$dir/.lumidify_archive_cksums";
+			my $fh;
+			if (!open $fh, ">>", $cksum_file) {
+				warn "ERROR: Unable to append to file \"$cksum_file\"!";
+				return;
+			}
+			print $fh, $cksum_output . " \"" . escape_filename $file . "\"\n";
+			close $fh;
+			$changed_files = 1;
 		}
-	}
-}
-
-sub add_new_files {
-	my $dir = shift;
-	my $new_files = shift;
-	my $cksum_tree = shift;
-	add_new_files_recurse($dir, $new_files, $cksum_tree);
+	}, sub {check_cksums $_[0], ".lumidify_archive_cksums.cksum";}, sub {
+		if ($changed_dirs) {
+			write_special_cksums $_[0], ".lumidify_archive_dirs";
+		}
+		if ($changed_files) {
+			write_special_cksums $_[0], ".lumidify_archive_files";
+		}
+	};
 }
 
-sub check_add_new_files {
+sub check_old_files {
 	my $dir = shift;
-	my $cksum_tree = shift;
-	my $new_files = check_new_files($dir, $cksum_tree);
-	add_new_files($dir, $new_files, $cksum_tree);
-	write_cksums($dir, $cksum_tree);
-}
-
-sub read_cksums_old {
-	my ($cksums, $dir, $recurse) = @_;
-	my $cksums_file = catfile($dir, '.lumidify_archive_cksums');
-	if (!-f $cksums_file) {
-		die("No cksum file '.lumidify_archive_cksums' in directory $dir\n");
-	}
-	open(my $fh, "<", $cksums_file) or die("Can't open $cksums_file: $!\n");
-	foreach (<$fh>) {
-		chomp;
-		next if (!$_);
-		my @fields = split(/ /, $_, 3);
-		if ($#fields != 2 || !looks_like_number($fields[0])) {
-			print("ERROR: Malformed line in $cksums_file:\n$_\n");
-			next;
-		}
-		$cksums->{$fields[2]} = $fields[0] . " " . $fields[1];
-	}
-	close($fh);
-	my $dirs_file = catfile($dir, '.lumidify_archive_dirs');
-	return if (!-f $dirs_file);
-	open($fh, "<", $dirs_file) or die("Can't open $dirs_file: $!\n");
-	foreach (<$fh>) {
-		chomp;
-		next if (!$_);
-		my $fulldir = catdir($dir, $_);
-		if (!-d $fulldir) {
-			print(STDERR "WARNING: Directory \"$fulldir\" mentioned in \"$dirs_file\" does not exist!\n");
-		}
-		$cksums->{$_} = {};
-		if ($recurse) {
-			read_cksums_old($cksums->{$_}, $fulldir, 1);
+	my $iter = make_lumia_iter $dir;
+	while (my $file = $iter->()) {
+		if (!-e $file) {
+			warn "Nonexistent file or directory: \"$file\"!";
 		}
 	}
-	close($fh);
 }
 
-sub read_cksums {
-	my ($cksums, $dir, $recurse) = @_;
-	my $cksums_file = catfile($dir, '.lumidify_archive_cksums');
-	if (!-f $cksums_file) {
-		die("No cksum file '.lumidify_archive_cksums' in directory $dir\n");
-	}
-	my $tmp_cksums = read_file($cksums_file, 1);
-	foreach (keys %$tmp_cksums) {
-		$cksums->{$_} = $tmp_cksums->{$_};
-	}
-	#$cksums->{keys %$tmp_cksums} = @{$tmp_cksums->{keys %$tmp_cksums}};
-=pod
-	open(my $fh, "<", $cksums_file) or die("Can't open $cksums_file: $!\n");
-	foreach (<$fh>) {
-		chomp;
-		next if (!$_);
-		my @fields = split(/ /, $_, 3);
-		if ($#fields != 2) {
-			print("ERROR: Malformed line in $cksums_file:\n$_\n");
-			next;
-		}
-		$cksums->{$fields[2]} = $fields[0] . " " . $fields[1];
-	}
-	close($fh);
-=cut
-	my $dirs_file = catfile($dir, '.lumidify_archive_dirs');
-	return if (!-f $dirs_file);
-	my $dirs = read_file($dirs_file);
-	foreach (keys %$dirs) {
-		my $fulldir = catdir($dir, $_);
-		if (!-d $fulldir) {
-			print(STDERR "WARNING: Directory \"$fulldir\" mentioned in \"$dirs_file\" does not exist!\n");
-		}
-		$cksums->{$_} = {};
-		if ($recurse) {
-			read_cksums($cksums->{$_}, $fulldir, 1);
-		}
+sub write_file {
+	my ($path, $contents, $is_cksum_file) = @_;
+	my $fh;
+	if (!open $fh, ">", $path) {
+		warn "ERROR: Unable to open \"$path\" for writing!";
+		return;
 	}
-=pod
-	open($fh, "<", $dirs_file) or die("Can't open $dirs_file: $!\n");
-	foreach (<$fh>) {
-		chomp;
-		next if (!$_);
-		my $fulldir = catdir($dir, $_);
-		if (!-d $fulldir) {
-			print(STDERR "WARNING: Directory \"$fulldir\" mentioned in \"$dirs_file\" does not exist!\n");
-		}
-		$cksums->{$_} = {};
-		if ($recurse) {
-			read_cksums($cksums->{$_}, $fulldir, 1);
+	foreach my $filename (keys %$contents) {
+		if ($is_cksum_file) {
+			print $fh "$contents->{$filename} ";
 		}
+		print $fh "\"" . escape_filename $filename . "\"\n";
 	}
-	close($fh);
-=cut
+	close $fh;
 }
 
-sub init_cksums_old {
-	my $dir = shift;
-	# avoid catfile turning it into an absolute path if $dir is an empty string
-	if (!$dir) {
-		$dir = ".";
+sub write_cksums {
+	my ($dir, $contents, $files_modified, $dirs_modified) = @_;
+	# No, this isn't efficient...
+	if ($files_modified) {
+		my %file_cksums = map {$_ => $contents->{$_}}, grep {defined $contents->{$_}}, keys %$contents;
+		write_file "$dir/.lumidify_archive_cksums", \%file_cksums, 1;
+		write_special_cksums $dir, ".lumidify_archive_files";
+	}
+	if ($dirs_modified) {
+		my %dir_cksums = map {$_ => undef}, grep {!defined $contents->{$_}}, keys %$contents;
+		write_file "$dir/.lumidify_archive_dirs", \%file_cksums;
+		write_special_cksums $dir, ".lumidify_archive_dirs";
 	}
-	my %cksums;
-	read_cksums_old(\%cksums, $dir, 1);
-	return \%cksums;
 }
 
-sub init_cksums {
+sub remove_old_files {
 	my $dir = shift;
-	# avoid catfile turning it into an absolute path if $dir is an empty string
-	if (!$dir) {
-		$dir = ".";
+	my $iter = make_lumia_iter $dir;
+	while (my $file = $iter->()) {
+		if (!-e $file) {
+			my $dir = dirname $file;
+			my $filename = basename $file;
+			my $lumia_dirs = read_file "$dir/.lumidify_archive_dirs";
+			if (defined $lumia_dirs && exists $lumia_dirs->{$filename}) {
+				delete $lumia_dirs->{$filename};
+				write_file $dir, $lumia_dirs;
+				print "Removed \"$file\" from \"$dir/.lumidify_archive_dirs\"\n";
+				write_special_cksums $dir, ".lumidify_archive_dirs";
+			} else {
+				my $lumia_files = read_file "$dir/.lumidify_archive_cksums", 1;
+				next if !defined $lumia_files;
+				delete $lumia_files->{$filename};
+				write_file $dir, $lumia_files, 1;
+				print "Removed \"$file\" from \"$dir/.lumidify_archive_files\"\n";
+				write_special_cksums $dir, ".lumidify_archive_cksums";
+			}
+		}
 	}
-	my %cksums;
-	read_cksums(\%cksums, $dir, 1);
-	return \%cksums;
 }
 
-# TODO: possibly sort files before dumping in files?
-sub write_cksums {
-	my ($dir, $cur_cksum_hash, $recurse, $create_dirs) = @_;
-	# FIXME: error checking
-	if ($create_dirs) {
-		make_path($dir);
-	}
-	my $cksums_path = catfile($dir, ".lumidify_archive_cksums");
-	my $dirs_path = catfile($dir, ".lumidify_archive_dirs");
-	open(my $cksumsfh, ">", $cksums_path) or die("Can't open \"$cksums_path\" for writing: $!\n");
-	open(my $dirsfh, ">", $dirs_path) or die("Can't open \"$dirs_path\" for writing: $!\n");
-	foreach my $file (keys %$cur_cksum_hash) {
-		my $file_esc = $file;
-		$file_esc =~ s/\\/\\\\/g;
-		$file_esc =~ s/"/\\"/g;
-		if (ref($cur_cksum_hash->{$file}) eq "HASH") {
-			print($dirsfh "\"$file_esc\"\n");
-		} else {
-			print($cksumsfh "$cur_cksum_hash->{$file} \"$file_esc\"\n");
-		}
-	}
-	close($cksumsfh);
-	close($dirsfh);
-
-	open(my $fh, ">", "$cksums_path.cksum") or die("Cannot open $cksums_path.cksum for writing\n");
-	my $cksums_path_esc = shell_quote($cksums_path);
-	my $dirs_path_esc = shell_quote($dirs_path);
-	my $cksums_cksum = `$CKSUM_CMD $cksums_path_esc`;
-	my $dirs_cksum = `$CKSUM_CMD $dirs_path_esc`;
-	chomp($cksums_cksum);
-	chomp($dirs_cksum);
-	print($fh "$cksums_cksum \".lumidify_archive_cksums\"\n");
-	print($fh "$dirs_cksum \".lumidify_archive_dirs\"\n");
-	if (-f catfile($dir, '.lumidify_archive_ignore')) {
-		my $ignore_path_esc = shell_quote(catfile($dir, '.lumidify_archive_ignore'));
-		my $ignore_cksum = `$CKSUM_CMD $ignore_path_esc`;
-		chomp $ignore_cksum;
-		print($fh "$ignore_cksum \".lumidify_archive_ignore\"\n");
-	}
-	close($fh);
-
-	# For e.g. moving, we don't want to read and write all cksums since
-	# everything below the level being moved stays the same
-	return if (!$recurse);
-	# Use second pass for this so the files don't all stay open
-	foreach my $file (keys %$cur_cksum_hash) {
-		if (ref($cur_cksum_hash->{$file}) eq "HASH") {
-			write_cksums(catdir($dir, $file), $cur_cksum_hash->{$file}, 1, $create_dirs);
+sub sort_by_dir {
+	my %sorted_files;
+	foreach my $file (@_) {
+		if (!-e $file) {
+			warn "ERROR: Source file \"$file\" doesn't exist.\n";
+			next;
+		}
+		my $dir = dirname($file);
+		if (!exists($sorted_files{$dir})) {
+			$sorted_files{$dir} = [];
 		}
+		push(@{$sorted_files{$dir}}, basename($file));
 	}
+	return \%sorted_files;
 }
 
 # $src: list of source paths
 # $dst: destination directory or file (in latter case only one src is allowed)
 sub copy_files {
-	my $src = shift;
-	my $dst = shift;
+	my ($src, $dst) = @_;
 	my $dst_dir = $dst;
 	if (!-d $dst) {
-		$dst_dir = dirname($dst);
+		$dst_dir = dirname $dst;
 	}
 	my $diff_name = 0;
 	if (!-d $dst && !-d $src->[0]) {
@@ -520,44 +402,44 @@ sub copy_files {
 		$diff_name = 1;
 	}
 	if (system("cp", "-aiv", @$src, $dst)) {
-		die("ERROR while copying files\n");
-	}
-	# Separate files by current dir so the cksum and dir files only need to be opened once
-	my %src_files;
-	foreach my $src_file (@$src) {
-		my $dir = dirname($src_file);
-		if (!exists($src_files{$dir})) {
-			$src_files{$dir} = [];
-		}
-		push(@{$src_files{$dir}}, basename($src_file));
-	}
-	my %dst_cksums;
-	read_cksums(\%dst_cksums, $dst_dir, 0);
-	foreach my $src_dir (keys %src_files) {
-		my %src_cksums;
-		read_cksums(\%src_cksums, $src_dir, 0);
-		foreach my $src_file (@{$src_files{$src_dir}}) {
-			if (exists($src_cksums{$src_file})) {
+		die "ERROR while copying files\n";
+	}
+	my $src_sorted = sort_by_dir @$src;
+	my $dst_cksums = read_cksums $dst_dir;
+	return if !defined $dst_cksums;
+	my $files_touched = 0;
+	my $dirs_touched = 0;
+	foreach my $src_dir (keys %$src_sorted) {
+		my $src_cksums = read_cksums $src_dir;
+		next if !defined $src_cksums;
+		foreach my $src_file (@{$src_sorted->{$src_dir}}) {
+			my $src_path = "$src_dir/$src_file";
+			if (-d $src_path) {
+				my $dirs_touched = 1;
+			} else {
+				my $files_touched = 1;
+			}
+			if (exists $src_cksums->{$src_file}) {
 				if ($diff_name) {
-					$dst_cksums{basename($dst)} = $src_cksums{$src_file};
+					$dst_cksums->{basename $dst} = $src_cksums->{$src_file};
 				} else {
-					$dst_cksums{$src_file} = $src_cksums{$src_file};
+					$dst_cksums->{$src_file} = $src_cksums->{$src_file};
 				}
 			} else {
-				print(STDERR "WARNING: \"$dst_dir/$src_file\" not in cksum or directory list.\n");
+				warn "WARNING: \"$src_path\" not in cksum or directory list\n";
 			}
 		}
 	}
-	write_cksums($dst_dir, \%dst_cksums, 0);
+	write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched;
 }
 
 sub move_file {
 	my ($src, $dst) = @_;
 	if (-d $dst) {
-		$dst = catdir($dst, basename($src));
+		$dst .= "/" . basename($src);
 	}
 	if (-e $dst) {
-		print(STDERR "WARNING: \"$dst\" exists already. Do you want to replace it? ");
+		warn "WARNING: \"$dst\" exists already. Do you want to replace it?";
 		my $choice = <STDIN>;
 		chomp $choice;
 		if ($choice ne "y" && $choice ne "Y") {
@@ -569,59 +451,78 @@ sub move_file {
 }
 
 sub move_from_same_dir {
-	my ($src_dir, $src_files, $dst_dir) = @_;
-	my %src_cksums;
-	read_cksums(\%src_cksums, $src_dir, 0);
-	my %dst_cksums;
-	read_cksums(\%dst_cksums, $dst_dir, 0);
+	my ($src_dir, $src_files, $dst_cksums, $dst_dir) = @_;
+	my $src_cksums = read_cksums $src_dir;
+	return if !defined $src_cksums;
+	my $files_touched = 0;
+	my $dirs_touched = 0;
 	foreach my $src_file (@$src_files) {
-		my $fullpath = catfile($src_dir, $src_file);
+		my $fullpath = "$src_dir/$src_file";
+		if (cmp_path($fullpath, $dst_dir)) {
+			warn "ERROR: can't move \"$fullpath\" into \"$dst_dir\" (same dir)\n";
+			next;
+		}
 		if (my $err = move_file($fullpath, $dst_dir)) {
-			print(STDERR "$err\n");
+			warn "$err\n";
 			next;
 		}
-		if (exists($src_cksums{$src_file})) {
-			$dst_cksums{$src_file} = $src_cksums{$src_file};
-			delete($src_cksums{$src_file});
+		if (-d $fullpath) {
+			$dirs_touched = 1;
+		} else {
+			$files_touched = 1;
+		}
+		if (exists $src_cksums->{$src_file}) {
+			$dst_cksums->{$src_file} = $src_cksums{$src_file};
+			delete $src_cksums->{$src_file};
 		} else {
-			print(STDERR "WARNING: \"$dst_dir/$src_file\" not in cksum or directory list.\n");
+			warn "WARNING: \"$src_dir/$src_file\" not in cksum or directory list.\n";
 		}
 	}
-	write_cksums($src_dir, \%src_cksums, 0);
-	write_cksums($dst_dir, \%dst_cksums, 0);
+	write_cksums $src_dir, $src_cksums, $files_touched, $dirs_touched;
+	return ($files_touched, $dirs_touched);
 }
 
 sub move_rename {
 	my ($src, $dst) = @_;
-	my $src_dir = dirname($src);
-	my $dst_dir = dirname($dst);
-	my $src_file = basename($src);
-	my $dst_file = basename($dst);
-	my %src_cksums;
-	read_cksums(\%src_cksums, $src_dir, 0);
-	my %dst_cksums;
+	my $src_dir = dirname $src;
+	my $dst_dir = dirname $dst;
+	my $src_file = basename $src;
+	my $dst_file = basename $dst;
+
+	my $src_cksums = read_cksums $src_dir;
+	return if !defined $src_cksums;
+	my $dst_cksums = {};
 	# if a file is simply being renamed in the same dir, the cksums
 	# should only be loaded and written once
 	if ($src_dir eq $dst_dir) {
-		%dst_cksums = %src_cksums;
-		delete($dst_cksums{$src_file});
+		%$dst_cksums = %$src_cksums;
+		delete $dst_cksums->{$src_file};
 	} else {
-		read_cksums(\%dst_cksums, $dst_dir, 0);
+		$dst_cksums = read_cksums $dst_dir;
+		return if !defined $dst_cksums;
 	}
+
 	if (my $err = move_file($src, $dst)) {
-		print(STDERR "$err\n");
+		warn  "$err\n";
 		return;
 	}
-	if (exists($src_cksums{$src_file})) {
-		$dst_cksums{$dst_file} = $src_cksums{$src_file};
-		delete($src_cksums{$src_file});
+	my $files_touched = 0;
+	my $dirs_touched = 0;
+	if (-d $src) {
+		$dirs_touched = 1;
 	} else {
-		print(STDERR "WARNING: \"$dst\" not in cksum or directory list.\n");
+		$files_touched = 1;
 	}
+	if (exists($src_cksums->{$src_file})) {
+		$dst_cksums->{$dst_file} = $src_cksums->{$src_file};
+		delete $src_cksums->{$src_file};
+	} else {
+		warn "WARNING: \"$src\" not in cksum or directory list.\n";
+	}
+	write_cksums $dst_dir, $dst_cksums, $files_touched, $dirs_touched;
 	if ($src_dir ne $dst_dir) {
-		write_cksums($src_dir, \%src_cksums, 0);
+		write_cksums $src_dir, $src_cksums, $files_touched, $dirs_touched;
 	}
-	write_cksums($dst_dir, \%dst_cksums, 0);
 }
 
 sub cmp_path {
@@ -635,92 +536,83 @@ sub cmp_path {
 # $src: list of source paths
 # $dst: destination directory or file (in latter case only one src is allowed)
 sub move_files {
-	my $src = shift;
-	my $dst = shift;
+	my ($src, $dst) = @_;
 	foreach my $src_file (@$src) {
 		if ($src_file eq ".") {
-			die("Can't move current directory (.)\n");
+			die "Can't move current directory (.)\n";
 		}
 	}
 	if (!-d $dst && $#$src != 0) {
-		die("move: only one src argument allowed when dst is a file\n");
+		die "move: only one source argument allowed when destination is a file\n";
 	}
 	if (!-d $dst && !-d $src->[0]) {
-		move_rename($src->[0], $dst);
+		move_rename $src->[0], $dst;
 		return;
 	}
 	if (!-e $dst && -d $src->[0]) {
-		move_rename($src->[0], $dst);
+		move_rename $src->[0], $dst;
 		return;
 	}
 	if (-e $dst && !-d $dst && -d $src->[0]) {
-		die("move: can't move directory to file\n");
+		die "move: can't move directory to file\n";
 	}
 	# Separate files by current dir so the cksum and dir files only need to be opened once
-	my %src_files;
-	foreach my $src_file (@$src) {
-		if (!-e $src_file) {
-			die("Source file \"$src_file\" doesn't exist.\n");
-		}
-		if (cmp_path($src_file, $dst)) {
-			print(STDERR "ERROR: can't move \"$src_file\" into \"$dst\" (same dir)\n");
-			next;
-		}
-		my $dir = dirname($src_file);
-		if (!exists($src_files{$dir})) {
-			$src_files{$dir} = [];
-		}
-		push(@{$src_files{$dir}}, basename($src_file));
-	}
-	foreach my $src_dir (keys %src_files) {
-		move_from_same_dir($src_dir, $src_files{$src_dir}, $dst);
-	}
+	my $src_files = sort_by_dir @$src;
+	my $dst_cksums = read_cksums $dst;
+	return if !defined $dst_cksums;
+	my $files_touched = 0;
+	my $dirs_touched = 0;
+	foreach my $src_dir (keys %$src_files) {
+		my ($tmp_files_touched, $tmp_dirs_touched) = move_from_same_dir $src_dir, $src_files{$src_dir}, $dst_cksums, $dst;
+		$files_touched ||= $tmp_files_touched;
+		$dirs_touched ||= $tmp_dirs_touched;
+	}
+	write_cksums $dst, $dst_cksums, $files_touched, $dirs_touched;
 }
 
 sub remove_file_dir {
 	my $path = shift;
 	if (-d $path) {
-		remove_tree($path, {safe => 1}) or return "ERROR: can't remove \"$path\": $!";
+		remove_tree $path, {safe => 1} or return "ERROR: can't remove \"$path\": $!";
 	} else {
-		unlink($path, {safe => 1}) or return "ERROR: can't remove \"$path\": $!";
+		unlink $path or return "ERROR: can't remove \"$path\": $!";
 	}
 	return 0;
 }
 
 sub remove_from_same_dir {
-	my ($dir, $files) = @_;
-	my %cksums;
-	read_cksums(\%cksums, $dir, 0);
-	foreach my $file (@$files) {
-		my $fullpath = catfile($dir, $file);
+	my ($dir, @files) = @_;
+	my $cksums = read_cksums $dir;
+	return if !defined $cksums;
+	my $dirs_touched = 0;
+	my $files_touched = 0;
+	foreach my $file (@files) {
+		my $fullpath = "$dir/$file";
 		if (!-e $fullpath) {
-			print(STDERR "\"$fullpath\": No such file or directory.\n");
+			warn "\"$fullpath\": No such file or directory.\n";
 		}
 		if (my $err = remove_file_dir($fullpath)) {
-			print(STDERR "$err\n");
+			warn "$err\n";
 			next;
 		}
-		if (exists($cksums{$file})) {
-			delete($cksums{$file});
+		if (exists $cksums->{$file}) {
+			delete $cksums->{$file};
+			if (defined $cksums->{$file}) {
+				$files_touched = 1;
+			} else {
+				$dirs_touched = 1;
+			}
 		} else {
-			print(STDERR "WARNING: \"$file\" not in cksum or directory list.\n");
+			warn "WARNING: \"$file\" not in cksum or directory list.\n";
 		}
 	}
-	write_cksums($dir, \%cksums, 0);
+	write_cksums $dir, $cksums, $files_touched, $dirs_touched;
 }
 
 sub remove_files {
-	my $files = shift;
-	my %sorted_files;
-	foreach my $file (@$files) {
-		my $dir = dirname($file);
-		if (!exists($sorted_files{$dir})) {
-			$sorted_files{$dir} = [];
-		}
-		push(@{$sorted_files{$dir}}, basename($file));
-	}
-	foreach my $dir (keys %sorted_files) {
-		remove_from_same_dir($dir, $sorted_files{$dir});
+	my $sorted_files = sort_by_dir @_;
+	foreach my $dir (keys %$sorted_files) {
+		remove_from_same_dir($dir, @{$sorted_files{$dir}});
 	}
 }
 
@@ -728,15 +620,17 @@ sub make_dirs {
 	my @created_dirs;
 	foreach (@_) {
 		if (system("mkdir", $_)) {
-			print(STDERR "ERROR creating directory $_\n");
+			warn "ERROR creating directory $_\n";
+			next;
 		}
 		push(@created_dirs, $_);
 	}
 	# Separate files by current dir so the cksum and dir files only need to be opened once
 	my %dirs;
 	foreach my $dir (@created_dirs) {
-		# FiXME: seems a bit ugly (must write cksums in new dir for it to work later)
-		write_cksums($dir, {}, 0);
+		system("touch", "$dir/.lumidify_archive_cksums");
+		system("touch", "$dir/.lumidify_archive_dirs");
+		write_special_cksums $dir, ".lumidify_archive_cksums", ".lumidify_archive_dirs";
 		my $parent = dirname($dir);
 		if (!exists($dirs{$parent})) {
 			$dirs{$parent} = [];
@@ -744,12 +638,12 @@ sub make_dirs {
 		push(@{$dirs{$parent}}, basename($dir));
 	}
 	foreach my $parent (keys %dirs) {
-		my %cksums;
-		read_cksums(\%cksums, $parent, 0);
+		my $parent_dirs = read_file "$parent/.lumidify_archive_dirs";
+		next if !defined $parent_dirs;
 		foreach my $dir (@{$dirs{$parent}}) {
-			$cksums{$dir} = {};
+			$parent_dirs->{$dir} = "";
 		}
-		write_cksums($parent, \%cksums, 0);
+		write_file "$parent/.lumidify_archive_dirs", $parent_dirs;
 	}
 }
 
@@ -773,44 +667,32 @@ if ($ARGV[0] eq "mv") {
 	if ($#ARGV > 0) {
 		$dir = $ARGV[1];
 	}
-	my $cksums = init_cksums($dir);
-	check_add_new_files($dir, $cksums);
-	write_cksums($dir, $cksums, 1);
+	check_add_new_files($dir);
 } elsif ($ARGV[0] eq "cknew") {
 	my $dir = ".";
 	if ($#ARGV > 0) {
 		$dir = $ARGV[1];
 	}
-	my $cksums = init_cksums($dir);
-	my $new_files = check_new_files($dir, $cksums);
-	print(Dumper($new_files));
-} elsif ($ARGV[0] eq "init") {
+	check_new_files($dir);
+} elsif ($ARGV[0] eq "check") {
 	my $dir = ".";
 	if ($#ARGV > 0) {
 		$dir = $ARGV[1];
-	}
-	# FIXME: at least first recurse through dirs and check if any contain cksums
-	my $cksums = {};
-	check_add_new_files($dir, $cksums);
-	write_cksums($dir, $cksums, 1);
-} elsif ($ARGV[0] eq "check") {
-	if ($#ARGV > 0) {
-		my $dir = $ARGV[1];
 		if (!-d $dir) {
-			die("ERROR: Directory \"$dir\" does not exist.\n");
+			die "ERROR: Directory \"$dir\" does not exist.\n";
 		}
 		chdir($dir);
 	}
-	check_files();
+	check_files($dir);
 } elsif ($ARGV[0] eq "clean") {
+	my $dir = ".";
 	if ($#ARGV > 0) {
-		my $dir = $ARGV[1];
+		$dir = $ARGV[1];
 		if (!-d $dir) {
-			die("ERROR: Directory \"$dir\" does not exist.\n");
+			die "ERROR: Directory \"$dir\" does not exist.\n";
 		}
-		chdir($dir);
 	}
-	clean_files();
+	clean_files($dir);
 } elsif ($ARGV[0] eq "extract") {
 	my $src_dir = ".";
 	my $dst_dir;
@@ -843,11 +725,4 @@ if ($ARGV[0] eq "mv") {
 	}
 	my @dirs = @ARGV[1..$#ARGV];
 	make_dirs(@dirs);
-} elsif ($ARGV[0] eq "convert") {
-	my $dir = ".";
-	if ($#ARGV > 0) {
-		$dir = $ARGV[1];
-	}
-	my $cksums = init_cksums_old($dir);
-	write_cksums($dir, $cksums, 1);
 }