#!/usr//bin/perl # $Id: syncdir,v 1.8 1997/10/04 11:17:53 yuuji Exp $ # Sync two directories' hierarchy # # (c)1995-1997 by HIROSE Yuuji [yuuji@ae.keio.ac.jp] # Last modified Mon Jun 30 11:01:16 1997 on crx # # [Commentary] # # This script enables you to make a perfect mirror of a certain directory. # # [How to use] # # Just call syncdir as follows: # # % syncdir SrcDir DestDir # # Syncdir reads `SrcDir' as copy source directory and copy each file # that is not in `DestDir' or is newer than that in `DestDir'. If `-d' # option is given, syncdir removes all files in `DestDir' that are not # in `SrcDir'. Thus `syncdir -d A B' can keep both A and B directories # strictly the same. Type `syncdir -h' alone to display other available # options. # # # 【なにこれ】 # # syncdir - 二つのディレクトリを同一に保つ # # # 【どやって使うの?】 # # % syncdir 複製元ディレクトリ 複製先ディレクトリ # # と起動すると、複製元ディレクトリ以下の全てのファイルのうち複製先ディレ # クトリに存在しない、または存在しても複製元のほうが新しいものだけを全て # 複製先ディレクトリにコピーします。-d オプションを付けると、複製先にし # か存在しないファイルがあった場合それを削除します。syncdir -d A B とす # るとAディレクトリとBディレクトリの中身を完全に同一に保つことができます。 # ただし、複製元ディレクトリ、複製先ディレクトリは必ず両方とも存在しなけ # ればなりません。 # # -n オプションをつけると、実際にはコピー/削除は行わず、どういう作業をす # るかだけを標準出力に表示します。syncdir -n A B | tee hoge としてどうい # うファイルがコピーされるか確認したあと、cat hoge | sh などとすると良い # かもしれません(【バグ】の項参照)。 # # -N オプションは、ほぼ -n と同じですが、複製先ディレクトリの「どのファ # イルを消すか」のみを調べて表示します。複製元/複製先の指定を間違えて逆 # にした場合古いファイルで新しいものを上書きする事故はありえませんが、新 # しく作ったはずのファイルを消してしまうことは考えられるので、消去チェッ # クだけを行います。 # # -p オプションをつけると複製元のファイルの atime (access time) を保存し # ます。atimeを気にする方は -p をつけてください。 # # -q オプションをつけるとメッセージを表示せずに実行します。しかし、この # スクリプトが完全に動作することが分かったら -q をデフォルトにして -v オ # プションでメッセージを出すように変える予定です。と、思っていたんですが、 # やっぱり心配症なのでメッセージがないと不安です私。表示するをデフォルト # にさせておいて下さい。 # # -l オプションをつけると、複製元の、ファイルを指しているシンボリックリ # ンク、をシンボリックリンクファイルとしてコピーせずに、そのシンボリック # リンクが指しているファイルをコピーします。 # # -L オプションをつけると、複製元の、ディレクトリを指しているシンボリッ # クリンク、をシンボリックリンクとしてコピーせずに、そのシンボリックリン # クが指しているディレクトリに降りてさらに再帰コピー作業を続けます。 # # -e オプションをつけると、複製元のリンク先を持たないファイルもそのまま # コピーします。 # # -x オプションに続けて正規表現(perlのもの)を指定すると、その正規表現に # マッチするファイル/ディレクトリは無視します。perlの正規表現なので、た # とえば a というファイルを無視したい場合は `-x ^a$' のように指定してく # ださい。単に a とすると a という文字を持つファイル全てを除外してしまい # ます。なお、-x オプションは複数指定できます。 # # # 【すすんだ使い方】 # # syncdir -pd a b なんてやって、間違って a が空っぽな時には b も空っぽに # なってしまいます。そんなわけで a, b を指定するのに結構ドキドキしてしま # います。そこで以下のようなファイルを ~/.syncdir という名前で書いておき # ます。 # # home: src=/home/yuuji;dest=/mo/mirror/home;keyfile=.cshrc # work: src=/private/yuuji/work;dest=/mo/mirror/private/work # keyfile=Makefile;exclude=^trashbox$ # # 書式は、 # # ボリューム名: src=複製元ディレクトリ;dest=複製先ディレクトリ # keyfile=認証ファイル # exclude=除外パターン # # 「認証ファイル」が複製元ディレクトリになかった場合には、そこは本来ある # べき姿の複製元ディレクトリではないと判定し、作業を中止します。例えば、 # ホームディレクトリが事故により破壊されていて ~ の下に何もなくなってし # まった場合にも複製先を破壊せずに済みます。「除外パターン」については # -x オプションと同様perlの正規表現で指定してください。 # # さて、~/.syncdir を用意したら以下のように、-V オプションでボリューム名 # を指定します。 # # % syncdir -pd -V home # あるいは # % syncdir -pd -r -V home # # 上の書式はボリューム名がhomeのものを読み込んでコピー/消去を行い、下の # 書式はボリューム名がhomeのものを読み込んで、src, dest を反転して作業を # 行います。さらに # # % syncdir -pd -a # # のように -a オプションをつけると、~/.syncdir に登録してある全てのボリュー # ムに対してコピー作業を行います。また、ボリューム名は -V home,www のよ # うに ,(カンマ)で区切って複数個指定することができます(したがってカンマ # を含むボリューム名は設定できません)。 # # また、~/.syncdir ファイルのボリューム定義には別のボリューム名を列挙す # ることができます。たとえば以下のようにボリュームのグルーピングができま # す。 # Mon: work # Tue: src # Wed: foo,bar # weekly: work,src,foo,bar # # work: src=~/work; dest=/mo/work; keyfile=Makefile # src: src=~/src; dest=/mo/src; keyfile=RCS # foo: src=~/foo; dest=/mo/foo; keyfile=foo.c # bar: src=~/bar; dest=/mo/bar; keyfile=bar.pl # # # ボリューム指定ファイルとして ~/.syncdir 以外のものを利用したい場合は、 # # % syncdir -f ~/.syncdir_2 -V hoge # # のように -f オプションに続けてボリューム指定ファイルを記述してください。 # # # 【バグ】 # # デバイスファイル/ソケットなどの特殊ファイルは扱うことができません。し # たがって /dev などのバックアップ目的には使わないでください。 # # ハードリンクされているファイルをコピーする時、リンク相手が複製元ディレ # クトリ配下に存在しない場合、複製先のファイルはハードリンクを持たない # (リンクカウント1の)ファイルとなります。ただし、その後複製元ディレクト # リとしてハードリンク相手を包含する位置を指定して再度syncdirを起動する # と、複製先の対応ファイルは正しいリンク相手を持つように修正されます。 # # -n オプションで表示するものは、二つのディレクトリの階層構造が違う時に # は実際に行う処理を正確に反映したものではありません。 # # ファイル名の先頭あるいは末尾に空白/タブ/改行文字を含むものを取り扱うこ # とが出来ません。これはPerlの仕様です。Macintoshで作成したファイルには # そうしたものが存在することがあるので注意して下さい。 # # 【ちうい】 # # mhのフォルダを sort したり pack した時にはsyncdirをしないで下さい! (し # てもいいけど) syncdirはファイルのタイムスタンプをみてコピーしているの # で、mhユーティリティがファイル名変更してタイムスタンプが変わらないまま # のものをsyncdirしても期待した結果となりません。わたしこれでハマりまし # た。 # # 【おまけ】 # # zsh で syncdir の引数/オプション補完を行なう記述です。~/.zshrcに放り込 # んでご利用下さい。 # # glob_syncdir () { #ボリューム定義ファイルからボリューム名補完 # local n a p=1 table=${SYNCDIRTABLE:-~/.syncdir} lo # read -cA a #コマンドライン全体を配列 a に入れる # read -cn n #引数の数を n に入れる # while [[ $p -lt $n ]] { # [[ -f $a[$p] && "$lo" = "-f" ]] && table=$a[$p] # lo=$a[$p] # p=$[++p] # } # reply=(`sed -n "/[^A-z]/s/:.*//p" $table`) # } # compctl -g "(.|)*(-/)" -x \ # 's[-]' -k (d l L e n N p x q f V r a) - \ # 'c[-1,-f]' -f - \ # 'c[-1,-V]' -K glob_syncdir -- syncdir # # 【謝辞】 # # 以下の方々に御協力を頂きました。ここに感謝申し上げます。 # ・伊野田尚史さん(立命館大学) # ・毛利こういちさん(立命館大学) # ・鈴木博史さん(テレビ朝日) # ・HONDA Takashi さん(NTT) # ・田中良知さん # ・岡田靖則さん(千葉大) # ・bsdusersメイリングリストのみなさん # # 【免責】 # # このスクリプトは完全に動作することを願って作られていますが、その保証は # できません。このスクリプトによってもたらされた結果については作者は責任 # を負いかねますのでご注意ください。とくにこのスクリプトの特長でもある d # オプションは、ファイル消去を伴うため、誤操作だけでなく作者の予想してい # ない特殊な環境での利用は危険を及ぼす可能性があります。このスクリプトを # つかってミラーリングしたいと思うディレクトリはかなり大切な場所でしょう # から、最初に利用する時は(別手段で)コピーディレクトリをつくってそこで試 # してみるなどしてください。 ($myname= $0) =~ s,.*[/\\],,; $quiet=0; $debug=0; $unix = (eval 'symlink("","");', $@ eq ''); $table=$ENV{'SYNCDIRTABLE'} || "~/.syncdir"; $usage = <<_eou_; Synchronize two directories. Usage: $myname [-Options] SrcDir DestDir or $myname [-Options] -V VolumeName or $myname [-Options] -a Options are... -d Delete files in DestDir that are nonexistent in SrcDir -l Chase files symlinks point to -L Chase directories symlinks point to -e Copy Empty symlink files -n No execute, print jobs to stdout -N Same as -n, except -N displays only files to be removed -p Preserve access time of source files -x PAT Exclude files that match PAT as perl-regexp -q Quiet (Both SrcDir and DestDir directories should exist.) Options for the second or third form are... -f TableFile Use TableFile as syncdir table file -V VolumeName Do the job for the volume VolumeName -r Swap src and dest for each volume -a Do the job for all volumes If -V or -a option is given, $myname reads ~/.syncdir for a table file. A table file's format is as follows: home: src=/usr/home/yuuji;dest=/mo/backup/home keyfile=.cshrc;exclude=^trashbox\$ where `src=' and `dest=' specify the source and destination directories, `keyfile=' specifies the file which is used to confirm the source directory's validity, `exclude=' specifies what -x option does. Multiple volume names can be specified by being delimited with comma as `-V home,www,src'. _eou_ #' while ($_ = $ARGV[0], /^-.+/ && shift) { last if /^--$/; while (/^-[A-z]/) { if (/^-p/) { #$backupdir = shift; $atimepr++; } elsif (/^-f/) { $table = shift; } elsif (/^-d/) { $delete++; } elsif (/^-D/) { $debug++; } elsif (/^-n/) { $noexec++; $quiet++; } elsif (/^-N/) { $noexec++; $quiet++; $noupdate++; } elsif (/^-l/) { $chaselinkfile++; } elsif (/^-L/) { $chaselinkdir++; } elsif (/^-e/) { $emptylink++; } elsif (/^-q/) { $quiet++; } elsif (/^-v/) { $quiet=0; } elsif (/^-V/) { push(@volume, split(",", shift)); $_=''; } elsif (/^-r/) { $reverse++; } elsif (/^-a/) { $all++; } elsif (/^-x/) { @exclude = (@exclude, shift); } else { print $usage; exit 0; } s/^-.(.*)/-$1/; } } &readtable if (@volume || $all); &setall if $all; sub syncvol { local(@volume) = @_; foreach $i (@volume) { &parsesyncdir($i); if ($reverse) { $i=$srcdir; $srcdir=$dstdir; $dstdir=$i; } if ($srcdir && $dstdir) { printf("#s=%s\n#d=%s\n#k=%s\n#x=%s\n", $srcdir, $dstdir, $keyfile, join(",", @exclude)) if (!$quiet || $noexec); &syncdir($srcdir, $dstdir, $keyfile); } } } if (@volume) { &syncvol(@volume); } else { &syncdir($ARGV[0], $ARGV[1]); } # Done. # -------------------- Subfunctions -------------------- sub syncdir { local($src, $dst, $keyfile) = @_; unless ($src && -d $src && $dst && -d $dst) { warn "$myname: Two existent directories must be specified.\n"; die "Type `$myname -h' to show help.\n"; } if ($keyfile && ! -e "$srcdir/$keyfile") { warn "Keyfile `$srcdir/$keyfile' not found. Skip this volume.\n"; return; } $exptn = join('|', @exclude); @visiteddirs=%linkfiles=(); &updatecopy($src, $dst) unless ($noupdate); @visiteddirs=(); &rmsync($src, $dst) if $delete; } # Copy newer files. sub updatecopy { local($src, $dst, $file, @dir) = @_; local($umask, $s, $d, $di, $lf) = (umask); return if ($unix && &loopcheck($src, $dst)); print "Synchronizing directory [$src] with [$dst]...\n" if $debug; opendir(SRCDIR, $src); @dir = readdir(SRCDIR); closedir(SRCDIR); foreach $file (@dir) { next if ($file =~ /^\.\.?$/); next if ($file =~ /$exptn/); $s = "$src/$file"; $d = "$dst/$file"; if (-d "$s") { ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime1, $ctime, $blksize, $bloks) = stat(_); # stat($s) if (!$chaselinkdir && $unix && -l $s) { # if src is symlink &buildlink($s, $d); } else { if ($unix && -l $d) { #if though $s is real directory, $d is symlink, #remove symlink first if ($noexec) { print "rm $d\n"; } else { unlink($d); } } umask(000); if (!-d $d) { if (-f $d) { # if $d is normal file, remove it if ($noexec) { print "rm $d\n"; } elsif (!unlink($d)) { warn "Cannot unlink [$d]\n"; next; } } if ($noexec) { print "mkdir $d\n"; } elsif (! mkdir("$d", $mode)) { warn "Cannot create [$d]\n"; next; } else { # Successful mkdir chown($uid, $gid, $d); # chmod($mode, $d); #moved below utime($atime, $mtime1, $d); print "mkdir $d\n" unless $quiet; } } umask($umask); chmod($mode, $d) unless $noexec; &updatecopy($s, $d); } } elsif (-f $s) { ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $bloks) = stat(_); # stat($s) if (!$chaselinkfile && $unix && -l $s) { # if src is symlink &buildlink($s, $d); } elsif ($nlink > 1) { # if src has hard links if ($linkfiles{"$dev*$ino"}) { # already accessed ($di, $lf) = ($linkfiles{"$dev*$ino"} =~ m/([^,]*),(.*)$/); if (! -f $d) { if ($noexec) { print "ln $lf, $d\n"; } else { print "$lf => $d\n" unless $quiet; link($lf, $d); } } else { ($dev2, $ino2) = stat(_); # stat($d) if ("$dev2*$ino2" ne $di) { if ($noexec) { print "rm -f $d\n"; print "ln $lf $d\n"; } else { print "$lf => $d\n" unless $quiet; unlink($d); link($lf, $d); } } } } else { # first access to this file if (! -f $d) { &cp($s, $d, $mode, $uid, $gid, $atime, $mtime, $size); } ($dev2, $ino2, $mode2, $nlink, $uid2, $gid2, $rdev, $size2, $atime2, $mtime2, $ctime, $blksize, $bloks) = stat(_);#$d if ($mtime>$mtime2 || ($mtime==$mtime2 && $size>$size2)) { &cp($s, $d, $mode, $uid, $gid, $atime, $mtime, $size); } elsif ($mode != $mode2) { unless ($noexec) { chmod($mode, $d); } if ($noexec || !$quiet) { printf "chmod %o $d\n", $mode; } } ($dev2, $ino2) = stat(_); # stat($d) # remember new file's devnum, inum and path name $linkfiles{"$dev*$ino"} = "$dev2*$ino2,$d"; } } else { # if src is normal file with link count 1 if (-l $d) { # if dest is symlink remove it first! if ($noexec) { print "rm $d\n"; &cp($s, $d); # to display job } else { unlink($d); } } if (! -f $d) { &cp($s, $d, $mode, $uid, $gid, $atime, $mtime, $size); } else { ($dev, $ino, $mode2, $nlink, $uid2, $gid2, $rdev, $size2, $atime2, $mtime2, $ctime, $blksize, $bloks) = stat(_);#$d if ($mtime>$mtime2 || ($mtime==$mtime2 && $size>$size2)) { &cp($s, $d, $mode, $uid, $gid, $atime, $mtime, $size); } elsif ($mode !=$mode2) { unless ($noexec) { chmod($mode, $d); } if ($noexec || !$quiet) { printf "chmod %o $d\n", $mode; } } } } } elsif (! -e $s && $emptylink) { &buildlink($s, $d); } else { warn "$s: not found\n"; } } } # Copy file preserving modtime and owner/group. sub cp { local($src, $dst, $mode, $uid, $gid, $atime, $mtime, $size) = @_; local($dir, $dirmode, $success); $currentfile = $dst; print "$src -> $dst\n" unless $quiet; if ($noexec) { print "cp -p $src $dst\n"; return; } #system("cp -p $src $dst"); open(SRC, "<$src") || warn "Cannot open $src to read.\n"; binmode(SRC); chmod(0700, $d) if (-d $dst); if (-d $dst) { system("rm -rf '$dst'"); } else { unlink("$dst"); } if (open(DST, ">$dst")) { $success=1; } else { ($dir) = ($dst =~ m,(.*)/(.*),); $dirmode = (stat($dir))[2]; unless (($dirmode & 00200) && ($dirmode&00100)) { $dirmode |= 0300; chmod($dirmode, $dir); print STDERR "Adding w/x permission to $dir\n"; } if (open(DST, ">$dst")) { $success=1; } else { warn "Cannot open $dst to write.\n"; } } if ($success) { utime(0, 0, $dst); # Set dst's time ancient until finish writing. binmode(DST); $SIG{'INT'} = 'interrupt'; print DST while ($_=, $_ ne ""); # 1.8(97/6/26) close(DST); chown($uid, $gid, $dst) if $unix; chmod($mode, $dst); utime($atime, $mtime, $src) if $atimepr; utime($atime, $mtime, $dst); if ((stat($dst))[7] != $size) { print STDERR "$dst: Write failed!\n"; unlink($dst); } $SIG{'INT'} = 'DEFAULT'; } close(SRC); } sub interrupt { unlink($currentfile) if (-f $currentfile); die "Abort.\n"; } # Remove destinations dir's file nonexistent in source dir sub rmsync { local($src, $dst, $f, @entry) = @_; return if ($unix && &loopcheck($src, $dst)); print "Checking nonexistent files in [$dst]...\n" if $debug; opendir(DIR, $dst); @entry=readdir(DIR); closedir(DIR); foreach $f (@entry) { next if ($f =~ /$exptn/); next if ($f =~ /^\.\.?$/); if (-d "$dst/$f" && (! $unix || ! -l "$src/$f")) { if (-d "$src/$f") { &rmsync("$src/$f", "$dst/$f"); } else { print "Removing destination dir [$dst/$f]\n" unless $quiet; if ($noexec) { print "rm -rf $dst/$f\n"; } else { system("rm -rf '$dst/$f'"); } } } else { unless (-e "$src/$f" || ($unix && -l "$src/$f")) { print "Removing destination file [$dst/$f]\n" unless $quiet; if ($noexec) { print "rm -f $dst/$f\n"; } else { unlink("$dst/$f") || warn "Cannot unlink $dst/$f\n"; } } } } } sub buildlink { local($src, $dest, $link) = @_; local($ui, $gi, $at, $mt); $link = readlink($src); unless (-l $dest && readlink($d) eq $link) { if ($noexec) { print "rm -rf $d\n" if (-e $d); print "ln -s $link $d\n"; } else { #unlink($d); #in case if $d is directory, we call rm -rf system("rm -rf '$d'"); symlink($link, $d) || warn "Cannot create link $d\n"; ($ui, $gi, $at, $mt) = (lstat($src))[4,5,8,9]; chown($ui, $gi, $d); # utime($at, $mt, $d); # Is this impossible?? print "${d}\@ -> $link\n" unless $quiet; } } } sub loopcheck { local($d, $D); foreach $d (@_) { ($dev, $ino) = stat($d); $D = "$ino*$dev"; if (grep($_ eq $D, @visiteddirs)) { warn "Already visited to [$src]. Maybe link loop($dev, $ino).\n"; return 1; } @visiteddirs = (@visiteddirs, $D); } return 0; } sub readtable { $table =~ s/~/$ENV{'HOME'}/; unless (@contents) { open(TABLE, $table) || die "No $table found.\n"; @contents = ; close(TABLE); } } sub setall { undef @volume; foreach (@contents) { if (/^([^:]+):/) { @volume = (@volume, $1); } } } sub parsesyncdir { local($volume) = @_; local($var, $val, @c, $l); $srcdir = $dstdir = $keyfile = @exclude = (); # 97/2/3 @c=@contents; for ($l=0; $l<@c && $c[$l] !~ /^$i:/; $l++) {} for (; $l<@c; $l++) { $_ = $c[$l]; last if (/^.+:/ && !/^$i:/); chop; if (/$i:\s*([^\#=]+)$/) { # recursive volume call $val = $1; &syncvol(split(/, */, $val)); $srcdir = $dstdir = ''; } else { while (s/([a-z]+=)([^;]+)//) { if (/^[ \t]*\#/) {$_=''; last;} $var="$1", $val=$2; $val =~ s,~/,$ENV{'HOME'}/,; $srcdir=$val if ($var =~ m/src/); $dstdir=$val if ($var =~ m/dest/); $keyfile=$val if ($var =~ m/keyfile/); @exclude=(@exclude, $val) if ($var =~ m/exclude/); } } } } __END__ Local Variables: fill-prefix: "# " End: