add directory-examining recursive compare derp(1)

This commit is contained in:
cinap_lenrek 2012-11-02 20:28:12 +01:00
parent 5cb6be9ce3
commit c0c2660f74
2 changed files with 606 additions and 0 deletions

132
sys/man/1/derp Normal file
View file

@ -0,0 +1,132 @@
.TH DERP 1
.SH NAME
derp \- directory-examining recursive compare
.SH SYNOPSIS
.B derp
[
.B -qcutDL
] [
.B -p
.I perms
]
.I myfile
.I oldfile
.I yourfile
.SH DESCRIPTION
.I Derp
recursively compares the two directories
.I myfile
and
.I yourfile
using a third common backup directory
.I oldfile
as reference. The changes found are printed
to standard output, one per line, with the file
status describing either side followed by tabulator
and the relative file path.
.LP
The possible status codes:
.TP
.B an
File added in
.I myfile
.TP
.B na
File added in
.I yourfile
.TP
.B aa!
Both sides added different files with the
same name
.TP
.B mn
File was modified in
.I myfile
.TP
.B nm
File was modified in
.I yourfile
.TP
.B mm!
File was changed differently in
.I myfile
and
.I yourfile
.TP
.B dn
File was deleted in
.I myfile
.TP
.B nd
File was deleted in
.I yourfile
.TP
.B md!
File was modified in
.I myfile
but deleted in
.I yourfile
.TP
.B dm!
File was modified in
.I yourfile
but deleted in
.I myfile
.LP
Errors are printed to standard error unless
.B -q
option is specified. The program is terminated
when errors are encountered unless the
.B -c
option is given. This can be usefull if files
are not accessible due to file permission or
media corruption.
.PP
The
.B -u
option will consider changes of file owner and group.
When omited, file ownership is ignored.
.PP
The
.B -p
option sets the octal mask
.I perms
of bits to check in the file permissions. The default
ignores file permissions.
.PP
When modification times are comparable then the
.B -t
option can be used to quickly find changes. If specified,
files are considered unchanged if the name, file size and
the modification time matches. This is usefull when
comparing /n/dump archives on the same fileserver.
.PP
Files are considered the same if they are from the
same mount and ther
.B qid
(see
.IR stat (5))
matches. For directories, the access time is also
compared. If the access time was disabled on the
fileserver, then all directories need to be compared
using the
.B -D
option.
.PP
Some filesystems like
.IR hgfs (4)
do not always return exact file size in stat, so
the length check can be disabled with the
.B -L
option.
.SH SOURCE
.B /sys/src/cmd/derp.c
.SH "SEE ALSO"
.IR cmp (1),
.IR diff (1),
.IR history (1),
.IR fs (4),
.IR hgfs (4)
.SH DIAGNOSTICS
The exit status is set to 'errors' when
errors where encountered.

474
sys/src/cmd/derp.c Normal file
View file

@ -0,0 +1,474 @@
#include <u.h>
#include <libc.h>
int permcheck = 0;
int usercheck = 0;
int dumpcheck = 1;
int sizecheck = 1;
int timecheck = 0;
int quiet = 0;
int errors = 0;
int noerror = 0;
void
error(char *fmt, ...)
{
char buf[ERRMAX];
va_list a;
errors++;
if(!quiet){
va_start(a, fmt);
vsnprint(buf, sizeof(buf), fmt, a);
va_end(a);
fprint(2, "%s: %s\n", argv0, buf);
}
if(!noerror)
exits("errors");
}
#pragma varargck argpos error 1
enum {
BUFSIZE = 8*1024,
};
int
cmpfile(char *a, char *b)
{
static uchar buf1[BUFSIZE], buf2[BUFSIZE];
int r, n, m, fd1, fd2;
if((fd1 = open(a, OREAD)) < 0)
error("can't open %s: %r", a);
if((fd2 = open(b, OREAD)) < 0)
error("can't open %s: %r", b);
r = fd1 != fd2;
if(fd1 >= 0 && fd2 >= 0)
do{
if((n = readn(fd1, buf1, sizeof(buf1))) < 0){
error("can't read %s: %r", a);
break;
}
m = n;
if(m == 0)
m++; /* read past eof to verify size */
if((m = readn(fd2, buf2, m)) < 0){
error("can't read %s: %r", b);
break;
}
if(m != n)
break;
if(n == 0){
r = 0;
break;
}
} while(memcmp(buf1, buf2, n) == 0);
if(fd1 >= 0)
close(fd1);
if(fd2 >= 0)
close(fd2);
return r;
}
int
samefile(Dir *a, Dir *b)
{
if(a == b)
return 1;
if(a->type == b->type && a->dev == b->dev &&
a->qid.type == b->qid.type &&
a->qid.path == b->qid.path &&
a->qid.vers == b->qid.vers){
if((a->qid.type & QTDIR) == 0)
return 1;
/*
* directories in /n/dump have the same qid, but
* atime can be used to skip potentially
* untouched subtrees.
*/
if(dumpcheck && a->atime == b->atime)
return 1;
}
return 0;
}
int
dcmp(Dir *a, Dir *b)
{
if(a == nil || b == nil)
return a != b;
if(samefile(a, b))
return 0;
if((a->qid.type | b->qid.type) & QTDIR)
return 1;
if((a->mode ^ b->mode) & permcheck)
return 1;
if(usercheck){
if(strcmp(a->uid, b->uid) != 0)
return 1;
if(strcmp(a->gid, b->gid) != 0)
return 1;
}
if(sizecheck)
if(a->length != b->length)
return 1;
if(timecheck)
if(a->mtime != b->mtime)
return 1;
if(sizecheck && timecheck)
return 0;
return cmpfile(a->name, b->name);
}
Dir*
statdir(char *path)
{
Dir *d;
d = dirstat(path);
if(d == nil)
error("can't stat %s: %r", path);
else
d->name = strdup(path);
return d;
}
Dir*
absdir(Dir *d, char *path)
{
if(d != nil)
d->name = smprint("%s/%s", path, d->name);
return d;
}
void
cleardir(Dir *d)
{
if(d != nil){
free(d->name);
d->name = "";
}
}
void
freedir(Dir *d)
{
cleardir(d);
free(d);
}
int
dnamecmp(void *a, void *b)
{
return strcmp(((Dir*)a)->name, ((Dir*)b)->name);
}
int
readifdir(Dir **dp)
{
int n, fd;
Dir *d;
d = *dp;
*dp = nil;
if(d == nil || (d->qid.type & QTDIR) == 0)
return 0;
fd = open(d->name, OREAD);
if(fd < 0){
error("can't open %s: %r", d->name);
return -1;
}
if((n = dirreadall(fd, dp)) < 0)
error("can't read %s: %r", d->name);
close(fd);
if(n > 1)
qsort(*dp, n, sizeof(Dir), dnamecmp);
return n;
}
void
diffgen(Dir *ld, Dir *rd, Dir *ad, char *path);
void
diffdir(Dir *ld, Dir *rd, Dir *ad, char *path)
{
int n, m, o, i, j, k, t, v;
char *sp, *lp, *rp, *ap;
Dir *od;
lp = rp = ap = nil;
if(ld != nil)
lp = ld->name;
if(rd != nil)
rp = rd->name;
if(ad != nil){
/* check if ad is the same as ld or rd */
if(ld != nil && samefile(ad, ld)){
ap = ld->name;
ad = nil; /* don't read directory twice */
}
else if(rd != nil && samefile(ad, rd)){
ap = rd->name;
ad = nil; /* don't read directory twice */
}
else
ap = ad->name;
}
n = readifdir(&ld);
m = readifdir(&rd);
if(n <= 0 && m <= 0)
return;
/* at least one side is directory */
o = readifdir(&ad);
i = j = k = 0;
for(;;){
if(i < n)
t = (j < m) ? strcmp(ld[i].name, rd[j].name) : -1;
else if(j < m)
t = 1;
else
break;
od = nil;
if(t < 0){
sp = smprint("%s/%s", path, ld[i].name);
if(ap == lp)
od = &ld[i];
else while(k < o){
v = strcmp(ad[k].name, ld[i].name);
if(v == 0){
od = absdir(&ad[k++], ap);
break;
} else if(v > 0)
break;
k++;
}
diffgen(absdir(&ld[i], lp), nil, od, sp);
cleardir(&ld[i]);
if(&ld[i] == od)
od = nil;
i++;
} else {
sp = smprint("%s/%s", path, rd[j].name);
if(ap == rp)
od = &rd[j];
else while(k < o){
v = strcmp(ad[k].name, rd[j].name);
if(v == 0){
od = absdir(&ad[k++], ap);
break;
} else if(v > 0)
break;
k++;
}
if(t > 0)
diffgen(nil, absdir(&rd[j], rp), od, sp);
else {
if(ap == lp)
od = &ld[i];
diffgen(absdir(&ld[i], lp), absdir(&rd[j], rp), od, sp);
cleardir(&ld[i]);
if(&ld[i] == od)
od = nil;
i++;
}
cleardir(&rd[j]);
if(&rd[j] == od)
od = nil;
j++;
}
cleardir(od);
free(sp);
}
free(ld);
free(rd);
free(ad);
}
void
diffgen(Dir *ld, Dir *rd, Dir *ad, char *path)
{
char *p;
if(dcmp(ld, rd) == 0)
return;
p = nil;
if(ld == nil || rd == nil){
/* one side doesnt exit anymore */
if(ad != nil){
/* existed before, is deletion */
if(ld != nil && (ad->qid.type & QTDIR) && (ld->qid.type & QTDIR)){
/* remote deleted direcotry, remote newer */
p = smprint("nd\t%s\n", path);
} else if(rd != nil && (ad->qid.type & QTDIR) && (rd->qid.type & QTDIR)){
/* local deleted direcotry, local newer */
p = smprint("dn\t%s\n", path);
} else if(dcmp(rd, ad) == 0){
/* local deleted file, local newer */
print("dn\t%s\n", path);
} else if(dcmp(ld, ad) == 0){
/* remote deleted file, remote newer */
print("nd\t%s\n", path);
} else if(ld != nil){
/* local modified, remote deleted, conflict */
print("md!\t%s\n", path);
} else {
/* remote modified, local deleted, conflict */
print("dm!\t%s\n", path);
}
} else {
/* didnt exist before, is addition */
if(ld != nil){
/* local added file, local newer */
print("an\t%s\n", path);
} else {
/* remote added file, remote newer */
print("na\t%s\n", path);
}
}
} else {
if(ad != nil){
/* existed before, is modification */
if((ad->qid.type & QTDIR) && (ld->qid.type & QTDIR) && (rd->qid.type & QTDIR)){
/* all still directories, no problem */
} else if(dcmp(rd, ad) == 0){
/* local modified file, local newer */
print("mn\t%s\n", path);
} else if(dcmp(ld, ad) == 0){
/* remote modified file, remote newer */
print("nm\t%s\n", path);
} else {
/* local and remote modified, conflict */
print("mm!\t%s\n", path);
}
} else {
/* didnt exist before, is addition from both */
if((ld->qid.type & QTDIR) && (rd->qid.type & QTDIR)){
/* local and remote added directories, no problem */
} else {
/* local and remote added files, conflict */
print("aa!\t%s\n", path);
}
}
}
diffdir(ld, rd, ad, path);
if(p != nil){
print("%s", p);
free(p);
}
}
void
diff3(char *lp, char *ap, char *rp)
{
Dir *ld, *rd, *ad;
char *name;
rd = ad = nil;
if((ld = statdir(lp)) == nil)
goto Out;
if((rd = statdir(rp)) == nil)
goto Out;
if(strcmp(ap, lp) == 0)
ad = ld;
else if(strcmp(ap, rp) == 0)
ad = rd;
else if((ad = statdir(ap)) == nil)
goto Out;
else if(samefile(ad, ld)){
freedir(ad);
ad = ld;
}
else if(samefile(ad, rd)){
freedir(ad);
ad = rd;
}
if(ld->qid.type & QTDIR)
name = ".";
else {
if(name = strrchr(lp, '/'))
name++;
else
name = lp;
}
diffgen(ld, rd, ad, name);
Out:
freedir(ld);
freedir(rd);
if(ad != nil && ad != ld && ad != rd)
freedir(ad);
}
void
usage(void)
{
fprint(2, "usage: %s [ -qcutDL ] [ -p perms ] myfile oldfile yourfile\n", argv0);
exits("usage");
}
void
main(int argc, char *argv[])
{
ARGBEGIN {
case 'q':
quiet = 1;
break;
case 'c':
noerror = 1;
break;
case 'u':
usercheck = 1;
break;
case 't':
timecheck = 1;
break;
case 'D':
dumpcheck = 0;
break;
case 'L':
sizecheck = 0;
break;
case 'p':
permcheck = strtol(EARGF(usage()), nil, 8) & 0777;
break;
default:
usage();
} ARGEND;
if(argc != 3)
usage();
diff3(argv[0], argv[1], argv[2]);
if(errors)
exits("errors");
exits(0);
}