#!/usr/bin/perl # Filename: lnR # Author: David Ljung Madison # See License: http://MarginalHacks.com/License my $VERSION= 1.02; # Description: Recursively links a bunch of files with a matching dir structure # Can keep a "history" so you can remove files from the new # link structure and not have them relinked when you run again. # Inspiration: For history: Brett Stauffer use strict; ################################################## # Setup the variables ################################################## my $PROGNAME = $0; $PROGNAME =~ s|.*/||; my $HISTORY_FILE = ".$PROGNAME.manifest"; umask 022; # 0755 use Cwd qw(abs_path getcwd); ################################################## # Usage ################################################## sub usage { my $msg; foreach $msg (@_) { print STDERR "ERROR: $msg\n"; } print STDERR < Recursively copy a bunch of directories, using links for files -ignore Files/dirs matching this regexp will be ignored -s Use symbolic links -v Verbose -history Keep history (so you can remove 'to' portions and run again) -history= Name the history file Use absolute path for 'from' if you want absolute links for -s USAGE exit -1; } sub parse_args { my ($from,$to,%opt); while ($#ARGV>=0) { my $arg=shift(@ARGV); if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-v$/) { $opt{verbose}=1; next; } if ($arg =~ /^-s/) { $opt{symbolic}=1; next; } if ($arg =~ /^-ignore/) { $opt{ignore}=shift(@ARGV); next; } if ($arg =~ /^-history(=(.+))?/) { $opt{history}= $2 ? $2 : $HISTORY_FILE; next; } if ($arg =~ /^-/) { usage("Unknown option: $arg"); } if (!$from) { $from = $arg; } elsif (!$to) { $to = $arg; } else { usage("Too many locations specified [$arg, $from, $to]"); } } usage("No from location defined") unless $from; usage("From location needs to be a directory") unless -d $from; usage("No to location defined") unless $to; (\%opt,$from,$to) } ################################################## # Manifest/history file ################################################## sub read_manifest { my ($opt, $from, $to) = @_; return unless $opt->{history}; open(MAN,"<$to/$opt->{history}") || return; while () { chomp; $opt->{manifest}{old}{$_}=1; } close MAN; } sub write_manifest { my ($opt, $from, $to) = @_; return unless $opt->{history}; open(MAN,">$to/$opt->{history}") || return print STDERR "[$PROGNAME] WARNING: Couldn't write [$to/$opt->{history}]\n"; foreach my $f ( keys %{$opt->{manifest}{new}} ) { print MAN "$f\n"; } close MAN; } ################################################## # Handle a directory ################################################## # Figure out the path from $to to where $from is sub back { my ($opt,$from,$to) = @_; # Don't need to fixup path for hard links return "" if !$opt->{symbolic}; # Absolute paths return $from if $from =~ m|^/|; # Assume the last element is the destination my $to_path = $to; $to_path = $to_path || "/"; usage("Can't find destination location [$to_path]") unless -d $to_path; my @from_path = split('/',abs_path($from)); my @to_path = split('/',abs_path($to_path)); # Find the highest common directory while ($from_path[0] eq $to_path[0] && @from_path) { shift @from_path; shift @to_path; } # Go back for each element left in to_path my $back = "../" x (@to_path); # And go up all the elements left in from_path $back . join('/',@from_path); } sub do_dir { my ($opt, $from, $to, $back) = @_; print "do_dir $from -> $to\n" if $opt->{verbose}; return print STDERR "[$PROGNAME] Couldn't read directory [$from]\n" unless (opendir(FROM,$from)); my @from = grep(!/^\.{1,2}$/,readdir(FROM)); closedir(FROM); @from = grep(!/$opt->{ignore}/,@from) if $opt->{ignore}; my @from_files = grep(-f "$from/$_", @from); my @from_dirs = grep(-d "$from/$_", @from); my %from_files; foreach ( @from_files ) { $from_files{$_} = 1; } my %from_dirs; foreach ( @from_dirs ) { $from_dirs{$_} = 1; } my @to; if (opendir(TO,$to)) { @to = grep(!/^\.{1,2}$/,readdir(TO)); closedir(TO); } my @to_links = grep(-l "$to/$_", @to); my @to_dirs = grep(-d "$to/$_", @to); my %to_links; foreach ( @to_links ) { $to_links{$_} = 1; } my %to_dirs; foreach ( @to_dirs ) { $to_dirs{$_} = 1; } # Clean out to directory first foreach my $link ( @to_links ) { unless ($from_files{$link}) { print "Remove $to/$link\n" if $opt->{verbose}; print STDERR "[$PROGNAME] Couldn't remove old link [$to/$link]\n" unless unlink "$to/$link"; } } foreach my $dir ( @to_dirs ) { unless ($from_dirs{$dir}) { print "Remove $to/$dir/\n" if $opt->{verbose}; system("/bin/rm -Rf \Q$to/$dir\E") if ($to ne "" && $dir ne ""); print STDERR "[$PROGNAME] Couldn't remove old directory [$to/$dir]\n$!\n" if ($?); } } # Build new links foreach my $link ( @from_files ) { my $to_link = "$to/$link"; # Add to the history manifest next if $to_link eq "$to/$opt->{history}"; $opt->{manifest}{new}{$to_link}=1; # Skip it.. next if -l $to_link; if (-e $to_link) { print STDERR "[$PROGNAME] Warning: File exists [$to_link]\n"; next; } # History skip - it was in the manifest, so we saw it already, # but it isn't in the 'to' hierarchy, so they removed it print STDERR "[$PROGNAME] Skipping removed file [$to_link]\n" if $opt->{manifest}{old}{$to_link}; print STDERR "[$PROGNAME] Skipping removed file [$to_link]\n" if $opt->{manifest}{old}{$to_link} && $opt->{verbose}; next if $opt->{manifest}{old}{$to_link}; if ($opt->{symbolic}) { print "Link $back/$link -> $to\n" if $opt->{verbose}; print STDERR "[$PROGNAME] Couldn't link [$to_link]\n" unless symlink("$back/$link",$to_link); } else { print "Link $from/$link -> $to\n" if $opt->{verbose}; print STDERR "[$PROGNAME] Couldn't link [$to_link]\n" unless link("$from/$link",$to_link); } } my $abs = ($back =~ m|^/|) ? 1 : 0; my $new_back = $abs ? $back : "../$back"; # Build new directories foreach my $dir ( @from_dirs ) { my $to_dir = "$to/$dir"; # Add to the history manifest $opt->{manifest}{new}{$to_dir}=1; if (!-d $to_dir) { print STDERR "[$PROGNAME] Skipping removed dir [$to_dir]\n" if $opt->{manifest}{old}{$to_dir}; # History skip (see above) print STDERR "[$PROGNAME] Skipping removed dir [$to_dir]\n" if $opt->{manifest}{old}{$to_dir} && $opt->{verbose}; next if $opt->{manifest}{old}{$to_dir}; print "mkdir $to_dir\n" if $opt->{verbose}; mkdir($to_dir,0755) || die("[$PROGNAME] Couldn't make new directory [$to_dir]\n"); } do_dir($opt,"$from/$dir",$to_dir,"$new_back/$dir"); } } ################################################## # Main code ################################################## sub main { my ($opt,$from,$to) = parse_args(); (-d $to) || mkdir("$to",0755) || die("[$PROGNAME] Couldn't make new directory [$to]\n"); read_manifest($opt, $from, $to); do_dir($opt, $from, $to, back($opt, $from, $to)); write_manifest($opt, $from, $to); } main();