The patches for 3.0.1pre3.
[rsync-patches.git] / db.diff
1 Added some DB-access routines to help rsync keep extra filesystem info
2 about the files it is dealing with.  This adds both the --db=CONFIG_FILE
3 option and the "db config" daemon parameter.
4
5 For the moment this only adds checksum caching when the --checksum option
6 is used.  Future improvments may include:
7
8  - Updating of MD5 checksums when transferring any file, even w/o -c.
9
10  - Caching of path info that allows for the finding of files to use for
11    moving/linking/copying/alternate-basis-use.
12
13  - Extend DB support beyond MySQL and SQLite (PostgreSQL?).
14
15 To use this patch, run these commands for a successful build:
16
17     patch -p1 <patches/remote-option.diff
18     patch -p1 <patches/db.diff
19     ./configure                               (optional if already run)
20     make
21
22 diff --git a/Makefile.in b/Makefile.in
23 --- a/Makefile.in
24 +++ b/Makefile.in
25 @@ -35,7 +35,7 @@ ZLIBOBJ=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \
26  OBJS1=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
27         util.o main.o checksum.o match.o syscall.o log.o backup.o
28  OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \
29 -       fileio.o batch.o clientname.o chmod.o acls.o xattrs.o
30 +       fileio.o batch.o clientname.o chmod.o db.o acls.o xattrs.o
31  OBJS3=progress.o pipe.o
32  DAEMON_OBJ = params.o loadparm.o clientserver.o access.o connection.o authenticate.o
33  popt_OBJS=popt/findme.o  popt/popt.o  popt/poptconfig.o \
34 diff --git a/checksum.c b/checksum.c
35 --- a/checksum.c
36 +++ b/checksum.c
37 @@ -23,6 +23,7 @@
38  
39  extern int checksum_seed;
40  extern int protocol_version;
41 +extern int use_db;
42  
43  int csum_length = SHORT_SUM_LENGTH; /* initial value */
44  
45 @@ -100,10 +101,10 @@ void get_checksum2(char *buf, int32 len, char *sum)
46         }
47  }
48  
49 -void file_checksum(char *fname, char *sum, OFF_T size)
50 +void file_checksum(const char *fname, STRUCT_STAT *st_p, char *sum)
51  {
52         struct map_struct *buf;
53 -       OFF_T i, len = size;
54 +       OFF_T i, len = st_p->st_size;
55         md_context m;
56         int32 remainder;
57         int fd;
58 @@ -114,7 +115,7 @@ void file_checksum(char *fname, char *sum, OFF_T size)
59         if (fd == -1)
60                 return;
61  
62 -       buf = map_file(fd, size, MAX_MAP_SIZE, CSUM_CHUNK);
63 +       buf = map_file(fd, len, MAX_MAP_SIZE, CSUM_CHUNK);
64  
65         if (protocol_version >= 30) {
66                 md5_begin(&m);
67 @@ -148,6 +149,9 @@ void file_checksum(char *fname, char *sum, OFF_T size)
68                 mdfour_result(&m, (uchar *)sum);
69         }
70  
71 +       if (use_db)
72 +               db_set_checksum(fname, st_p, sum);
73 +
74         close(fd);
75         unmap_file(buf);
76  }
77 diff --git a/cleanup.c b/cleanup.c
78 --- a/cleanup.c
79 +++ b/cleanup.c
80 @@ -27,6 +27,7 @@ extern int am_daemon;
81  extern int io_error;
82  extern int keep_partial;
83  extern int got_xfer_error;
84 +extern int use_db;
85  extern char *partial_dir;
86  extern char *logfile_name;
87  
88 @@ -124,6 +125,12 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)
89                 /* FALLTHROUGH */
90  #include "case_N.h"
91  
92 +               if (use_db)
93 +                       db_disconnect();
94 +
95 +               /* FALLTHROUGH */
96 +#include "case_N.h"
97 +
98                 if (cleanup_child_pid != -1) {
99                         int status;
100                         int pid = wait_process(cleanup_child_pid, &status, WNOHANG);
101 diff --git a/clientserver.c b/clientserver.c
102 --- a/clientserver.c
103 +++ b/clientserver.c
104 @@ -42,6 +42,7 @@ extern int numeric_ids;
105  extern int filesfrom_fd;
106  extern int remote_protocol;
107  extern int protocol_version;
108 +extern int always_checksum;
109  extern int io_timeout;
110  extern int no_detach;
111  extern int write_batch;
112 @@ -49,6 +50,7 @@ extern int default_af_hint;
113  extern int logfile_format_has_i;
114  extern int logfile_format_has_o_or_i;
115  extern mode_t orig_umask;
116 +extern char *db_config;
117  extern char *bind_address;
118  extern char *sockopts;
119  extern char *config_file;
120 @@ -782,6 +784,12 @@ static int rsync_module(int f_in, int f_out, int i, char *addr, char *host)
121         } else if (am_root < 0) /* Treat --fake-super from client as --super. */
122                 am_root = 2;
123  
124 +       db_config = lp_db_config(i);
125 +       if (!*db_config || (!always_checksum && protocol_version < 30))
126 +               db_config = NULL;
127 +       else
128 +               db_read_config(FLOG, db_config);
129 +
130         if (filesfrom_fd == 0)
131                 filesfrom_fd = f_in;
132  
133 diff --git a/configure.in b/configure.in
134 --- a/configure.in
135 +++ b/configure.in
136 @@ -969,6 +969,8 @@ if test x"$enable_acl_support" = x"no" -o x"$enable_xattr_support" = x"no" -o x"
137      fi
138  fi
139  
140 +LIBS="$LIBS -lmysqlclient -lsqlite3"
141 +
142  case "$CC" in
143  ' checker'*|checker*)
144      AC_DEFINE(FORCE_FD_ZERO_MEMSET, 1, [Used to make "checker" understand that FD_ZERO() clears memory.])
145 diff --git a/db.c b/db.c
146 new file mode 100644
147 --- /dev/null
148 +++ b/db.c
149 @@ -0,0 +1,557 @@
150 +/*
151 + * Routines to access extended file info via DB.
152 + *
153 + * Copyright (C) 2008 Wayne Davison
154 + *
155 + * This program is free software; you can redistribute it and/or modify
156 + * it under the terms of the GNU General Public License as published by
157 + * the Free Software Foundation; either version 3 of the License, or
158 + * (at your option) any later version.
159 + *
160 + * This program is distributed in the hope that it will be useful,
161 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
162 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
163 + * GNU General Public License for more details.
164 + *
165 + * You should have received a copy of the GNU General Public License along
166 + * with this program; if not, visit the http://fsf.org website.
167 + */
168 +
169 +#include "rsync.h"
170 +#include "ifuncs.h"
171 +
172 +#define USE_MYSQL
173 +#define USE_SQLITE
174 +
175 +#ifdef USE_MYSQL
176 +#include <mysql/mysql.h>
177 +#include <mysql/errmsg.h>
178 +#endif
179 +#ifdef USE_SQLITE
180 +#include <sqlite3.h>
181 +#endif
182 +
183 +extern int protocol_version;
184 +extern int checksum_len;
185 +
186 +#define DB_TYPE_NONE 0
187 +#define DB_TYPE_MYSQL 1
188 +#define DB_TYPE_SQLITE 2
189 +
190 +int use_db = DB_TYPE_NONE;
191 +
192 +static const char *dbhost = NULL, *dbuser = NULL, *dbpass = NULL, *dbname = NULL;
193 +static unsigned int dbport = 0;
194 +
195 +static union {
196 +#ifdef USE_MYSQL
197 +    MYSQL *mysql;
198 +#endif
199 +#ifdef USE_SQLITE
200 +    sqlite3 *sqlite;
201 +#endif
202 +    void *all;
203 +} dbh;
204 +
205 +#define SEL_DEV 0
206 +#define SEL_SUM 1
207 +#define REP_SUM 2
208 +#define MAX_PREP_CNT 3
209 +
210 +static union {
211 +#ifdef USE_MYSQL
212 +    MYSQL_STMT *mysql;
213 +#endif
214 +#ifdef USE_SQLITE
215 +    sqlite3_stmt *sqlite;
216 +#endif
217 +    void *all;
218 +} statements[MAX_PREP_CNT];
219 +
220 +static int md_num;
221 +static enum logcode log_code;
222 +
223 +static unsigned int bind_disk_id;
224 +static unsigned long long bind_devno, bind_ino, bind_size, bind_mtime, bind_ctime;
225 +static char bind_thishost[256], bind_sum[MAX_DIGEST_LEN];
226 +static int bind_thishost_len;
227 +
228 +static unsigned int prior_disk_id = 0;
229 +static unsigned long long prior_devno = 0;
230 +
231 +int db_read_config(enum logcode code, const char *config_file)
232 +{
233 +       char buf[2048], *cp;
234 +       FILE *fp;
235 +       int lineno = 0;
236 +
237 +       log_code = code;
238 +
239 +       bind_thishost_len = strlcpy(bind_thishost, "localhost", sizeof bind_thishost);
240 +
241 +       if (!(fp = fopen(config_file, "r"))) {
242 +               rsyserr(log_code, errno, "unable to open %s", config_file);
243 +               return 0;
244 +       }
245 +       while (fgets(buf, sizeof buf, fp)) {
246 +               lineno++;
247 +               if ((cp = strchr(buf, '#')) == NULL
248 +                && (cp = strchr(buf, '\r')) == NULL
249 +                && (cp = strchr(buf, '\n')) == NULL)
250 +                       cp = buf + strlen(buf);
251 +               while (cp != buf && isSpace(cp-1)) cp--;
252 +               *cp = '\0';
253 +
254 +               if (!*buf)
255 +                       continue;
256 +
257 +               if (!(cp = strchr(buf, ':')))
258 +                       goto invalid_line;
259 +               *cp++ = '\0';
260 +
261 +               while (isSpace(cp)) cp++;
262 +               if (strcasecmp(buf, "dbhost") == 0)
263 +                       dbhost = strdup(cp);
264 +               else if (strcasecmp(buf, "dbuser") == 0)
265 +                       dbuser = strdup(cp);
266 +               else if (strcasecmp(buf, "dbpass") == 0)
267 +                       dbpass = strdup(cp);
268 +               else if (strcasecmp(buf, "dbname") == 0)
269 +                       dbname = strdup(cp);
270 +               else if (strcasecmp(buf, "dbport") == 0)
271 +                       dbport = atoi(cp);
272 +               else if (strcasecmp(buf, "thishost") == 0)
273 +                       bind_thishost_len = strlcpy(bind_thishost, cp, sizeof bind_thishost);
274 +               else if (strcasecmp(buf, "dbtype") == 0) {
275 +#ifdef USE_MYSQL
276 +                       if (strcasecmp(cp, "mysql") == 0) {
277 +                               use_db = DB_TYPE_MYSQL;
278 +                               continue;
279 +                       }
280 +#endif
281 +#ifdef USE_SQLITE
282 +                       if (strcasecmp(cp, "sqlite") == 0) {
283 +                               use_db = DB_TYPE_SQLITE;
284 +                               continue;
285 +                       }
286 +#endif
287 +                       rprintf(log_code,
288 +                           "Unsupported dbtype on line #%d in %s.\n",
289 +                           lineno, config_file);
290 +                       use_db = DB_TYPE_NONE;
291 +                       return 0;
292 +               } else {
293 +                 invalid_line:
294 +                       rprintf(log_code, "Invalid line #%d in %s\n",
295 +                               lineno, config_file);
296 +                       use_db = DB_TYPE_NONE;
297 +                       return 0;
298 +               }
299 +       }
300 +       fclose(fp);
301 +
302 +       if (bind_thishost_len >= (int)sizeof bind_thishost)
303 +               bind_thishost_len = sizeof bind_thishost - 1;
304 +
305 +       if (!use_db || !dbname) {
306 +               rprintf(log_code, "Please specify at least dbtype and dbname in %s.\n", config_file);
307 +               use_db = DB_TYPE_NONE;
308 +               return 0;
309 +       }
310 +
311 +       md_num = protocol_version >= 30 ? 5 : 4;
312 +
313 +       return 1;
314 +}
315 +
316 +#ifdef USE_MYSQL
317 +static MYSQL_STMT *prepare_mysql(MYSQL_BIND *binds, int bind_cnt, const char *fmt, ...)
318 +{
319 +       va_list ap;
320 +       char *query;
321 +       int qlen, param_cnt;
322 +       MYSQL_STMT *stmt = mysql_stmt_init(dbh.mysql);
323 +
324 +       if (stmt == NULL)
325 +               out_of_memory("prepare_mysql");
326 +
327 +       va_start(ap, fmt);
328 +       qlen = vasprintf(&query, fmt, ap);
329 +       va_end(ap);
330 +       if (qlen < 0)
331 +               out_of_memory("prepare_mysql");
332 +
333 +       if (mysql_stmt_prepare(stmt, query, qlen) != 0) {
334 +               rprintf(log_code, "Prepare failed: %s\n", mysql_stmt_error(stmt));
335 +               return NULL;
336 +       }
337 +       free(query);
338 +
339 +       if ((param_cnt = mysql_stmt_param_count(stmt)) != bind_cnt) {
340 +               rprintf(log_code, "Parameters in statement = %d, bind vars = %d\n",
341 +                       param_cnt, bind_cnt);
342 +               return NULL;
343 +       }
344 +       if (bind_cnt)
345 +               mysql_stmt_bind_param(stmt, binds);
346 +
347 +       return stmt;
348 +}
349 +#endif
350 +
351 +#ifdef USE_MYSQL
352 +static int db_connect_mysql(void)
353 +{
354 +       MYSQL_BIND binds[10];
355 +
356 +       if (!(dbh.mysql = mysql_init(NULL)))
357 +               out_of_memory("db_read_config");
358 +
359 +       if (!mysql_real_connect(dbh.mysql, dbhost, dbuser, dbpass, dbname, dbport, NULL, 0))
360 +               return 0;
361 +
362 +       memset(binds, 0, sizeof binds);
363 +       binds[0].buffer_type = MYSQL_TYPE_LONGLONG;
364 +       binds[0].buffer = &bind_devno;
365 +       binds[1].buffer_type = MYSQL_TYPE_STRING;
366 +       binds[1].buffer = &bind_thishost;
367 +       binds[1].buffer_length = bind_thishost_len;
368 +       statements[SEL_DEV].mysql = prepare_mysql(binds, 2,
369 +               "SELECT disk_id"
370 +               " FROM disk"
371 +               " WHERE devno = ? AND host = ? AND mounted = 1");
372 +       if (!statements[SEL_DEV].mysql)
373 +               return 0;
374 +
375 +       memset(binds, 0, sizeof binds);
376 +       binds[0].buffer_type = MYSQL_TYPE_LONG;
377 +       binds[0].buffer = &bind_disk_id;
378 +       binds[1].buffer_type = MYSQL_TYPE_LONGLONG;
379 +       binds[1].buffer = &bind_ino;
380 +       binds[2].buffer_type = MYSQL_TYPE_LONGLONG;
381 +       binds[2].buffer = &bind_size;
382 +       binds[3].buffer_type = MYSQL_TYPE_LONGLONG;
383 +       binds[3].buffer = &bind_mtime;
384 +       binds[4].buffer_type = MYSQL_TYPE_LONGLONG;
385 +       binds[4].buffer = &bind_ctime;
386 +       statements[SEL_SUM].mysql = prepare_mysql(binds, 5,
387 +               "SELECT checksum"
388 +               " FROM inode_map"
389 +               " WHERE disk_id = ? AND ino = ? AND sum_type = %d"
390 +               "   AND size = ? AND mtime = ? AND ctime = ?",
391 +               md_num);
392 +       if (!statements[SEL_SUM].mysql)
393 +               return 0;
394 +
395 +       memset(binds, 0, sizeof binds);
396 +       binds[0].buffer_type = MYSQL_TYPE_LONG;
397 +       binds[0].buffer = &bind_disk_id;
398 +       binds[1].buffer_type = MYSQL_TYPE_LONGLONG;
399 +       binds[1].buffer = &bind_ino;
400 +       binds[2].buffer_type = binds[6].buffer_type = MYSQL_TYPE_LONGLONG;
401 +       binds[2].buffer = binds[6].buffer = &bind_size;
402 +       binds[3].buffer_type = binds[7].buffer_type = MYSQL_TYPE_LONGLONG;
403 +       binds[3].buffer = binds[7].buffer = &bind_mtime;
404 +       binds[4].buffer_type = binds[8].buffer_type = MYSQL_TYPE_LONGLONG;
405 +       binds[4].buffer = binds[8].buffer = &bind_ctime;
406 +       binds[5].buffer_type = binds[9].buffer_type = MYSQL_TYPE_BLOB;
407 +       binds[5].buffer = binds[9].buffer = &bind_sum;
408 +       binds[5].buffer_length = binds[9].buffer_length = checksum_len;
409 +       statements[REP_SUM].mysql = prepare_mysql(binds, 10,
410 +               "INSERT INTO inode_map"
411 +               " SET disk_id = ?, ino = ?, sum_type = %d,"
412 +               "     size = ?, mtime = ?, ctime = ?, checksum = ?"
413 +               " ON DUPLICATE KEY"
414 +               " UPDATE size = ?, mtime = ?, ctime = ?, checksum = ?",
415 +               md_num, md_num);
416 +       if (!statements[REP_SUM].mysql)
417 +               return 0;
418 +
419 +       return 1;
420 +}
421 +#endif
422 +
423 +#ifdef USE_SQLITE
424 +static int db_connect_sqlite(void)
425 +{
426 +       char *sql;
427 +
428 +#if 0
429 +       if (sqlite3_open_v2(dbname, &dbh.sqlite, SQLITE_OPEN_READWRITE, NULL) != 0)
430 +               return 0;
431 +#else
432 +       if (sqlite3_open(dbname, &dbh.sqlite) != 0)
433 +               return 0;
434 +#endif
435 +
436 +       sql = "SELECT disk_id"
437 +           " FROM disk"
438 +           " WHERE devno = ? AND host = ? AND mounted = 1";
439 +       if (sqlite3_prepare_v2(dbh.sqlite, sql, -1, &statements[SEL_DEV].sqlite, NULL) != 0)
440 +               return 0;
441 +
442 +       if (asprintf(&sql,
443 +           "SELECT checksum"
444 +           " FROM inode_map"
445 +           " WHERE disk_id = ? AND ino = ? AND sum_type = %d"
446 +           "   AND size = ? AND mtime = ? AND ctime = ?",
447 +           md_num) < 0
448 +        || sqlite3_prepare_v2(dbh.sqlite, sql, -1, &statements[SEL_SUM].sqlite, NULL) != 0)
449 +               return 0;
450 +       free(sql);
451 +
452 +       if (asprintf(&sql,
453 +           "INSERT OR REPLACE INTO inode_map"
454 +           " (disk_id, ino, sum_type, size, mtime, ctime, checksum)"
455 +           " VALUES(?, ?, %d, ?, ?, ?, ?)",
456 +           md_num) < 0
457 +        || sqlite3_prepare_v2(dbh.sqlite, sql, -1, &statements[REP_SUM].sqlite, NULL) != 0)
458 +               return 0;
459 +       free(sql);
460 +
461 +       return 1;
462 +}
463 +#endif
464 +
465 +int db_connect(void)
466 +{
467 +       switch (use_db) {
468 +#ifdef USE_MYSQL
469 +       case DB_TYPE_MYSQL:
470 +               if (db_connect_mysql())
471 +                       return 1;
472 +               break;
473 +#endif
474 +#ifdef USE_SQLITE
475 +       case DB_TYPE_SQLITE:
476 +               if (db_connect_sqlite())
477 +                       return 1;
478 +               break;
479 +#endif
480 +       }
481 +
482 +       rprintf(log_code, "Unable to connect to DB\n");
483 +       db_disconnect();
484 +       use_db = DB_TYPE_NONE;
485 +
486 +       return 0;
487 +}
488 +
489 +void db_disconnect(void)
490 +{
491 +       int ndx;
492 +
493 +       if (!dbh.all)
494 +               return;
495 +
496 +       for (ndx = 0; ndx < MAX_PREP_CNT; ndx++) {
497 +               if (statements[ndx].all) {
498 +                       switch (use_db) {
499 +#ifdef USE_MYSQL
500 +                       case DB_TYPE_MYSQL:
501 +                               mysql_stmt_close(statements[ndx].mysql);
502 +                               break;
503 +#endif
504 +#ifdef USE_SQLITE
505 +                       case DB_TYPE_SQLITE:
506 +                               sqlite3_finalize(statements[ndx].sqlite);
507 +                               break;
508 +#endif
509 +                       }
510 +                       statements[ndx].all = NULL;
511 +               }
512 +       }
513 +
514 +       switch (use_db) {
515 +#ifdef USE_MYSQL
516 +       case DB_TYPE_MYSQL:
517 +               mysql_close(dbh.mysql);
518 +               break;
519 +#endif
520 +#ifdef USE_SQLITE
521 +       case DB_TYPE_SQLITE:
522 +               sqlite3_close(dbh.sqlite);
523 +               break;
524 +#endif
525 +       }
526 +
527 +       dbh.all = NULL;
528 +}
529 +
530 +static MYSQL_STMT *exec_mysql(int ndx)
531 +{
532 +       MYSQL_STMT *stmt = statements[ndx].mysql;
533 +       int rc;
534 +
535 +       if ((rc = mysql_stmt_execute(stmt)) == CR_SERVER_LOST) {
536 +               db_disconnect();
537 +               if (db_connect()) {
538 +                       stmt = statements[ndx].mysql;
539 +                       rc = mysql_stmt_execute(stmt);
540 +               }
541 +       }
542 +       if (rc != 0) {
543 +               rprintf(log_code, "SQL execute failed: %s\n", mysql_stmt_error(stmt));
544 +               return NULL;
545 +       }
546 +
547 +       return stmt;
548 +}
549 +
550 +static int fetch_mysql(MYSQL_BIND *binds, int bind_cnt, int ndx)
551 +{
552 +       unsigned long length[32];
553 +       my_bool is_null[32], error[32];
554 +       MYSQL_STMT *stmt;
555 +       int i, rc;
556 +
557 +       if (bind_cnt > 32)
558 +               exit_cleanup(RERR_UNSUPPORTED);
559 +
560 +       if ((stmt = exec_mysql(ndx)) == NULL)
561 +               return 0;
562 +
563 +       for (i = 0; i < bind_cnt; i++) {
564 +               binds[i].is_null = &is_null[i];
565 +               binds[i].length = &length[i];
566 +               binds[i].error = &error[i];
567 +       }
568 +       mysql_stmt_bind_result(stmt, binds);
569 +
570 +       if ((rc = mysql_stmt_fetch(stmt)) != 0) {
571 +               if (rc != MYSQL_NO_DATA) {
572 +                       rprintf(log_code, "SELECT fetch failed: %s\n",
573 +                               mysql_stmt_error(stmt));
574 +               }
575 +               mysql_stmt_free_result(stmt);
576 +               return 0;
577 +       }
578 +
579 +       mysql_stmt_free_result(stmt);
580 +
581 +       return is_null[0] ? 0 : 1;
582 +}
583 +
584 +static void get_disk_id(unsigned long long devno)
585 +{
586 +       switch (use_db) {
587 +#ifdef USE_MYSQL
588 +       case DB_TYPE_MYSQL: {
589 +               MYSQL_BIND binds[1];
590 +
591 +               bind_devno = devno; /* The one variable SEL_DEV input value. */
592 +
593 +               /* Bind where to put the output. */
594 +               binds[0].buffer_type = MYSQL_TYPE_LONG;
595 +               binds[0].buffer = &prior_disk_id;
596 +               if (!fetch_mysql(binds, 1, SEL_DEV))
597 +                       prior_disk_id = 0;
598 +               break;
599 +           }
600 +#endif
601 +#ifdef USE_SQLITE
602 +       case DB_TYPE_SQLITE: {
603 +               sqlite3_stmt *stmt = statements[SEL_DEV].sqlite;
604 +               sqlite3_bind_int64(stmt, 1, devno);
605 +               sqlite3_bind_text(stmt, 2, bind_thishost, bind_thishost_len, SQLITE_STATIC);
606 +               if (sqlite3_step(stmt) == SQLITE_ROW)
607 +                       prior_disk_id = sqlite3_column_int(stmt, 0);
608 +               else
609 +                       prior_disk_id = 0;
610 +               sqlite3_reset(stmt);
611 +               break;
612 +           }
613 +#endif
614 +       }
615 +
616 +       prior_devno = devno;
617 +}
618 +
619 +int db_get_checksum(UNUSED(const char *fname), const STRUCT_STAT *st_p, char *sum)
620 +{
621 +       if (prior_devno != st_p->st_dev)
622 +               get_disk_id(st_p->st_dev);
623 +       if (prior_disk_id == 0)
624 +               return 0;
625 +
626 +       switch (use_db) {
627 +#ifdef USE_MYSQL
628 +       case DB_TYPE_MYSQL: {
629 +               MYSQL_BIND binds[1];
630 +
631 +               bind_disk_id = prior_disk_id;
632 +               bind_ino = st_p->st_ino;
633 +               bind_size = st_p->st_size;
634 +               bind_mtime = st_p->st_mtime;
635 +               bind_ctime = st_p->st_ctime;
636 +
637 +               binds[0].buffer_type = MYSQL_TYPE_BLOB;
638 +               binds[0].buffer = sum;
639 +               binds[0].buffer_length = checksum_len;
640 +               return fetch_mysql(binds, 1, SEL_SUM);
641 +           }
642 +#endif
643 +#ifdef USE_SQLITE
644 +       case DB_TYPE_SQLITE: {
645 +               sqlite3_stmt *stmt = statements[SEL_SUM].sqlite;
646 +               sqlite3_bind_int(stmt, 1, prior_disk_id);
647 +               sqlite3_bind_int64(stmt, 2, st_p->st_ino);
648 +               sqlite3_bind_int64(stmt, 3, st_p->st_size);
649 +               sqlite3_bind_int64(stmt, 4, st_p->st_mtime);
650 +               sqlite3_bind_int64(stmt, 5, st_p->st_ctime);
651 +               if (sqlite3_step(stmt) == SQLITE_ROW) {
652 +                       int len = sqlite3_column_bytes(stmt, 0);
653 +                       if (len > MAX_DIGEST_LEN)
654 +                               len = MAX_DIGEST_LEN;
655 +                       memcpy(sum, sqlite3_column_blob(stmt, 0), len);
656 +                       sqlite3_reset(stmt);
657 +                       return 1;
658 +               }
659 +               sqlite3_reset(stmt);
660 +               return 0;
661 +           }
662 +#endif
663 +       }
664 +
665 +       return 0;
666 +}
667 +
668 +int db_set_checksum(UNUSED(const char *fname), const STRUCT_STAT *st_p, const char *sum)
669 +{
670 +       if (prior_devno != st_p->st_dev)
671 +               get_disk_id(st_p->st_dev);
672 +       if (prior_disk_id == 0)
673 +               return 0;
674 +
675 +       switch (use_db) {
676 +#ifdef USE_MYSQL
677 +       case DB_TYPE_MYSQL: {
678 +               bind_disk_id = prior_disk_id;
679 +               bind_ino = st_p->st_ino;
680 +               bind_size = st_p->st_size;
681 +               bind_mtime = st_p->st_mtime;
682 +               bind_ctime = st_p->st_ctime;
683 +               memcpy(bind_sum, sum, checksum_len);
684 +
685 +               return exec_mysql(REP_SUM) != NULL;
686 +           }
687 +#endif
688 +#ifdef USE_SQLITE
689 +       case DB_TYPE_SQLITE: {
690 +               int rc;
691 +               sqlite3_stmt *stmt = statements[REP_SUM].sqlite;
692 +               sqlite3_bind_int(stmt, 1, prior_disk_id);
693 +               sqlite3_bind_int64(stmt, 2, st_p->st_ino);
694 +               sqlite3_bind_int64(stmt, 3, st_p->st_size);
695 +               sqlite3_bind_int64(stmt, 4, st_p->st_mtime);
696 +               sqlite3_bind_int64(stmt, 5, st_p->st_ctime);
697 +               sqlite3_bind_blob(stmt, 6, sum, checksum_len, SQLITE_TRANSIENT);
698 +               rc = sqlite3_step(stmt);
699 +               sqlite3_reset(stmt);
700 +               return rc == SQLITE_DONE;
701 +           }
702 +#endif
703 +       }
704 +
705 +       return 0;
706 +}
707 diff --git a/flist.c b/flist.c
708 --- a/flist.c
709 +++ b/flist.c
710 @@ -54,6 +54,7 @@ extern int preserve_devices;
711  extern int preserve_specials;
712  extern int uid_ndx;
713  extern int gid_ndx;
714 +extern int use_db;
715  extern int eol_nulls;
716  extern int relative_paths;
717  extern int implied_dirs;
718 @@ -1235,14 +1236,16 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
719                 memcpy(bp + basename_len, linkname, linkname_len);
720  #endif
721  
722 -       if (always_checksum && am_sender && S_ISREG(st.st_mode))
723 -               file_checksum(thisname, tmp_sum, st.st_size);
724 -
725         if (am_sender)
726                 F_PATHNAME(file) = pathname;
727         else if (!pool)
728                 F_DEPTH(file) = extra_len / EXTRA_LEN;
729  
730 +       if (always_checksum && am_sender && S_ISREG(st.st_mode)) {
731 +               if (!use_db || !db_get_checksum(thisname, &st, tmp_sum))
732 +                       file_checksum(thisname, &st, tmp_sum);
733 +       }
734 +
735         /* This code is only used by the receiver when it is building
736          * a list of files for a delete pass. */
737         if (keep_dirlinks && linkname_len && flist) {
738 @@ -1858,6 +1861,9 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
739                      | (eol_nulls || reading_remotely ? RL_EOL_NULLS : 0);
740         int implied_dot_dir = 0;
741  
742 +       if (use_db)
743 +               db_connect();
744 +
745         rprintf(FLOG, "building file list\n");
746         if (show_filelist_p())
747                 start_filelist_progress("building file list");
748 diff --git a/generator.c b/generator.c
749 --- a/generator.c
750 +++ b/generator.c
751 @@ -58,6 +58,7 @@ extern int update_only;
752  extern int ignore_existing;
753  extern int ignore_non_existing;
754  extern int inplace;
755 +extern int use_db;
756  extern int append_mode;
757  extern int make_backups;
758  extern int csum_length;
759 @@ -718,7 +719,8 @@ int unchanged_file(char *fn, struct file_struct *file, STRUCT_STAT *st)
760            of the file time to determine whether to sync */
761         if (always_checksum > 0 && S_ISREG(st->st_mode)) {
762                 char sum[MAX_DIGEST_LEN];
763 -               file_checksum(fn, sum, st->st_size);
764 +               if (!use_db || !db_get_checksum(fn, st, sum))
765 +                       file_checksum(fn, st, sum);
766                 return memcmp(sum, F_SUM(file), checksum_len) == 0;
767         }
768  
769 @@ -2161,6 +2163,9 @@ void generate_files(int f_out, const char *local_name)
770                         : "enabled");
771         }
772  
773 +       if (use_db && always_checksum)
774 +               db_connect();
775 +
776         /* Since we often fill up the outgoing socket and then just sit around
777          * waiting for the other 2 processes to do their thing, we don't want
778          * to exit on a timeout.  If the data stops flowing, the receiver will
779 diff --git a/loadparm.c b/loadparm.c
780 --- a/loadparm.c
781 +++ b/loadparm.c
782 @@ -126,6 +126,7 @@ typedef struct
783         char *auth_users;
784         char *charset;
785         char *comment;
786 +       char *db_config;
787         char *dont_compress;
788         char *exclude;
789         char *exclude_from;
790 @@ -177,6 +178,7 @@ static service sDefault =
791   /* auth_users; */             NULL,
792   /* charset; */                NULL,
793   /* comment; */                NULL,
794 + /* db_config; */              NULL,
795   /* dont_compress; */          DEFAULT_DONT_COMPRESS,
796   /* exclude; */                        NULL,
797   /* exclude_from; */           NULL,
798 @@ -307,6 +309,7 @@ static struct parm_struct parm_table[] =
799   {"auth users",        P_STRING, P_LOCAL, &sDefault.auth_users,        NULL,0},
800   {"charset",           P_STRING, P_LOCAL, &sDefault.charset,           NULL,0},
801   {"comment",           P_STRING, P_LOCAL, &sDefault.comment,           NULL,0},
802 + {"db config",         P_STRING, P_LOCAL, &sDefault.db_config,         NULL,0},
803   {"dont compress",     P_STRING, P_LOCAL, &sDefault.dont_compress,     NULL,0},
804   {"exclude from",      P_STRING, P_LOCAL, &sDefault.exclude_from,      NULL,0},
805   {"exclude",           P_STRING, P_LOCAL, &sDefault.exclude,           NULL,0},
806 @@ -400,6 +403,7 @@ FN_GLOBAL_INTEGER(lp_rsync_port, &Globals.rsync_port)
807  FN_LOCAL_STRING(lp_auth_users, auth_users)
808  FN_LOCAL_STRING(lp_charset, charset)
809  FN_LOCAL_STRING(lp_comment, comment)
810 +FN_LOCAL_STRING(lp_db_config, db_config)
811  FN_LOCAL_STRING(lp_dont_compress, dont_compress)
812  FN_LOCAL_STRING(lp_exclude, exclude)
813  FN_LOCAL_STRING(lp_exclude_from, exclude_from)
814 diff --git a/main.c b/main.c
815 --- a/main.c
816 +++ b/main.c
817 @@ -49,6 +49,7 @@ extern int copy_unsafe_links;
818  extern int keep_dirlinks;
819  extern int preserve_hard_links;
820  extern int protocol_version;
821 +extern int always_checksum;
822  extern int file_total;
823  extern int recurse;
824  extern int xfer_dirs;
825 @@ -73,6 +74,7 @@ extern char *partial_dir;
826  extern char *dest_option;
827  extern char *basis_dir[];
828  extern char *rsync_path;
829 +extern char *db_config;
830  extern char *shell_cmd;
831  extern char *batch_name;
832  extern char *password_file;
833 @@ -1482,6 +1484,9 @@ int main(int argc,char *argv[])
834                 exit_cleanup(RERR_SYNTAX);
835         }
836  
837 +       if (db_config && (always_checksum || protocol_version >= 30))
838 +               db_read_config(FERROR, db_config);
839 +
840         if (am_server) {
841                 set_nonblocking(STDIN_FILENO);
842                 set_nonblocking(STDOUT_FILENO);
843 diff --git a/options.c b/options.c
844 --- a/options.c
845 +++ b/options.c
846 @@ -92,6 +92,7 @@ int use_qsort = 0;
847  char *files_from = NULL;
848  int filesfrom_fd = -1;
849  char *filesfrom_host = NULL;
850 +char *db_config = NULL;
851  int eol_nulls = 0;
852  int protect_args = 0;
853  int human_readable = 0;
854 @@ -321,6 +322,7 @@ void usage(enum logcode F)
855    rprintf(F," -q, --quiet                 suppress non-error messages\n");
856    rprintf(F,"     --no-motd               suppress daemon-mode MOTD (see manpage caveat)\n");
857    rprintf(F," -c, --checksum              skip based on checksum, not mod-time & size\n");
858 +  rprintf(F,"     --db=CONFIG_FILE        specify a config file for FS DB\n");
859    rprintf(F," -a, --archive               archive mode; equals -rlptgoD (no -H,-A,-X)\n");
860    rprintf(F,"     --no-OPTION             turn off an implied OPTION (e.g. --no-D)\n");
861    rprintf(F," -r, --recursive             recurse into directories\n");
862 @@ -579,6 +581,7 @@ static struct poptOption long_options[] = {
863    {"checksum",        'c', POPT_ARG_VAL,    &always_checksum, 1, 0, 0 },
864    {"no-checksum",      0,  POPT_ARG_VAL,    &always_checksum, 0, 0, 0 },
865    {"no-c",             0,  POPT_ARG_VAL,    &always_checksum, 0, 0, 0 },
866 +  {"db",               0,  POPT_ARG_STRING, &db_config, 0, 0, 0 },
867    {"block-size",      'B', POPT_ARG_LONG,   &block_size, 0, 0, 0 },
868    {"compare-dest",     0,  POPT_ARG_STRING, 0, OPT_COMPARE_DEST, 0, 0 },
869    {"copy-dest",        0,  POPT_ARG_STRING, 0, OPT_COPY_DEST, 0, 0 },
870 diff --git a/pipe.c b/pipe.c
871 --- a/pipe.c
872 +++ b/pipe.c
873 @@ -26,6 +26,10 @@ extern int am_sender;
874  extern int am_server;
875  extern int blocking_io;
876  extern int filesfrom_fd;
877 +extern int always_checksum;
878 +extern int protocol_version;
879 +extern int use_db;
880 +extern char *db_config;
881  extern mode_t orig_umask;
882  extern char *logfile_name;
883  extern int remote_option_cnt;
884 @@ -141,6 +145,9 @@ pid_t local_child(int argc, char **argv, int *f_in, int *f_out,
885                         logfile_close();
886                 }
887  
888 +               use_db = 0;
889 +               db_config = NULL;
890 +
891                 if (remote_option_cnt) {
892                         int rc = remote_option_cnt + 1;
893                         const char **rv = remote_options;
894 @@ -148,6 +155,8 @@ pid_t local_child(int argc, char **argv, int *f_in, int *f_out,
895                                 option_error();
896                                 exit_cleanup(RERR_SYNTAX);
897                         }
898 +                       if (db_config && (always_checksum || protocol_version >= 30))
899 +                               db_read_config(FERROR, db_config);
900                 }
901  
902                 if (dup2(to_child_pipe[0], STDIN_FILENO) < 0 ||
903 diff --git a/receiver.c b/receiver.c
904 --- a/receiver.c
905 +++ b/receiver.c
906 @@ -43,11 +43,13 @@ extern int basis_dir_cnt;
907  extern int make_backups;
908  extern int cleanup_got_literal;
909  extern int remove_source_files;
910 +extern int always_checksum;
911  extern int append_mode;
912  extern int sparse_files;
913  extern int keep_partial;
914  extern int checksum_seed;
915  extern int inplace;
916 +extern int use_db;
917  extern int delay_updates;
918  extern mode_t orig_umask;
919  extern struct stats stats;
920 @@ -399,6 +401,9 @@ int recv_files(int f_in, char *local_name)
921         if (verbose > 2)
922                 rprintf(FINFO, "recv_files(%d) starting\n", cur_flist->used);
923  
924 +       if (use_db && !always_checksum)
925 +               db_connect();
926 +
927         if (delay_updates)
928                 delayed_bits = bitbag_create(cur_flist->used + 1);
929  
930 diff --git a/support/dbupdate b/support/dbupdate
931 new file mode 100755
932 --- /dev/null
933 +++ b/support/dbupdate
934 @@ -0,0 +1,281 @@
935 +#!/usr/bin/perl -w
936 +use strict;
937 +
938 +use DBI;
939 +use Getopt::Long;
940 +use Cwd qw(abs_path cwd);
941 +use Digest::MD4;
942 +use Digest::MD5;
943 +
944 +my $MOUNT_FILE = '/etc/mtab';
945 +
946 +&Getopt::Long::Configure('bundling');
947 +&usage if !&GetOptions(
948 +    'db=s' => \( my $db_config ),
949 +    'mounts|m' => \( my $update_mounts ),
950 +    'recurse|r' => \( my $recurse_opt ),
951 +    'check|c' => \( my $check_opt ),
952 +    'verbose|v+' => \( my $verbosity = 0 ),
953 +    'help|h' => \( my $help_opt ),
954 +);
955 +&usage if $help_opt || !defined $db_config;
956 +
957 +my %config;
958 +open(IN, '<', $db_config) or die "Unable to open $db_config: $!\n";
959 +while (<IN>) {
960 +    s/[#\r\n].*//s;
961 +    next if /^$/;
962 +    my($key, $val) = /^(\S+):\s*(.*)/ or die "Unable to parse line $. of $db_config\n";
963 +    $config{$key} = $val;
964 +}
965 +close IN;
966 +
967 +die "You must define at least dbtype and dbname in $db_config\n"
968 +    unless defined $config{'dbtype'} && defined $config{'dbname'};
969 +
970 +my $thishost = $config{'thishost'} || 'localhost';
971 +
972 +my $connect = 'DBI:' . $config{'dbtype'} . ':database=' . $config{'dbname'};
973 +$connect =~ s/:database=/:dbname=/ if $config{'dbtype'} eq 'SQLite';
974 +$connect .= ';host=' . $config{'dbhost'} if defined $config{'dbhost'};
975 +$connect .= ';port=' . $config{'dbport'} if defined $config{'dbport'};
976 +
977 +my $dbh = DBI->connect($connect, $config{'dbuser'}, $config{'dbpass'})
978 +    or die "DB connection failed\n";
979 +
980 +END {
981 +    $dbh->disconnect if defined $dbh;
982 +}
983 +
984 +my $sel_disk_H = $dbh->prepare("
985 +    SELECT disk_id, devno, mounted, comment
986 +    FROM disk
987 +    WHERE host = ?
988 +    ") or die $dbh->errstr;
989 +
990 +my $ins_disk_H = $dbh->prepare("
991 +    INSERT INTO disk
992 +    (devno, host, mounted, comment)
993 +    VALUES(?, ?, ?, ?)
994 +    ") or die $dbh->errstr;
995 +
996 +my $up_disk_H = $dbh->prepare("
997 +    UPDATE disk
998 +    SET mounted = ?
999 +    WHERE disk_id = ?
1000 +    ") or die $dbh->errstr;
1001 +
1002 +my $row_id = $config{'dbtype'} eq 'SQLite' ? 'ROWID' : 'ID';
1003 +my $sel_lastid_H = $dbh->prepare("
1004 +    SELECT LAST_INSERT_$row_id()
1005 +    ") or die $dbh->errstr;
1006 +
1007 +my $sel_sum_H = $dbh->prepare("
1008 +    SELECT sum_type, checksum
1009 +    FROM inode_map
1010 +    WHERE disk_id = ? AND ino = ? AND size = ? AND mtime = ? AND ctime = ?
1011 +    ") or die $dbh->errstr;
1012 +
1013 +my $rep_sum_H = $dbh->prepare("
1014 +    REPLACE INTO inode_map
1015 +    (disk_id, ino, size, mtime, ctime, sum_type, checksum)
1016 +    VALUES(?, ?, ?, ?, ?, ?, ?)
1017 +    ") or die $dbh->errstr;
1018 +
1019 +my %mounts;
1020 +if ($update_mounts) {
1021 +    open(IN, $MOUNT_FILE) or die "Unable to open $MOUNT_FILE: $!\n";
1022 +    while (<IN>) {
1023 +       my($devname, $mnt) = (split)[0,1];
1024 +       next unless $devname =~ m#^/dev#;
1025 +       my($devno) = (stat($mnt))[0];
1026 +       if (!defined $devno) {
1027 +           warn "Unable to stat $mnt: $!\n";
1028 +           next;
1029 +       }
1030 +       $mounts{$devno} = "$devname on $mnt";
1031 +    }
1032 +    close IN;
1033 +}
1034 +
1035 +my %disk_id;
1036 +$sel_disk_H->execute($thishost);
1037 +while (my($disk_id, $devno, $mounted, $comment) = $sel_disk_H->fetchrow_array) {
1038 +    if ($update_mounts) {
1039 +       if (defined $mounts{$devno}) {
1040 +           if ($comment ne $mounts{$devno}) {
1041 +               if ($mounted) {
1042 +                   $up_disk_H->execute(0, $disk_id);
1043 +               }
1044 +               next;
1045 +           }
1046 +           if (!$mounted) {
1047 +               $up_disk_H->execute(1, $disk_id);
1048 +           }
1049 +       } else {
1050 +           if ($mounted) {
1051 +               $up_disk_H->execute(0, $disk_id);
1052 +           }
1053 +           next;
1054 +       }
1055 +    } else {
1056 +       next unless $mounted;
1057 +    }
1058 +    $disk_id{$devno} = $disk_id;
1059 +}
1060 +$sel_disk_H->finish;
1061 +
1062 +if ($update_mounts) {
1063 +    while (my($devno, $comment) = each %mounts) {
1064 +       next if $disk_id{$devno};
1065 +       $ins_disk_H->execute($devno, $thishost, 1, $comment);
1066 +       $sel_lastid_H->execute;
1067 +       ($disk_id{$devno}) = $sel_lastid_H->fetchrow_array;
1068 +       $sel_lastid_H->finish;
1069 +    }
1070 +}
1071 +
1072 +my $start_dir = cwd();
1073 +
1074 +my @dirs = @ARGV;
1075 +@dirs = '.' unless @dirs;
1076 +foreach (@dirs) {
1077 +    $_ = abs_path($_);
1078 +}
1079 +
1080 +$| = 1;
1081 +
1082 +my $exit_code = 0;
1083 +
1084 +my $md4 = Digest::MD4->new;
1085 +my $md5 = Digest::MD5->new;
1086 +
1087 +while (@dirs) {
1088 +    my $dir = shift @dirs;
1089 +
1090 +    if (!chdir($dir)) {
1091 +       warn "Unable to chdir to $dir: $!\n";
1092 +       next;
1093 +    }
1094 +    if (!opendir(DP, '.')) {
1095 +       warn "Unable to opendir $dir: $!\n";
1096 +       next;
1097 +    }
1098 +
1099 +    my $reldir = $dir;
1100 +    $reldir =~ s#^$start_dir(/|$)# $1 ? '' : '.' #eo;
1101 +    print "$reldir ... \n" if $verbosity;
1102 +
1103 +    my @subdirs;
1104 +    while (defined(my $fn = readdir(DP))) {
1105 +       next if $fn =~ /^\.\.?$/ || -l $fn;
1106 +       if (-d _) {
1107 +           push(@subdirs, "$dir/$fn") unless $fn =~ /^(CVS|\.svn|\.git|\.bzr)$/;
1108 +           next;
1109 +       }
1110 +       next unless -f _;
1111 +
1112 +       my($dev,$ino,$size,$mtime,$ctime) = (stat(_))[0,1,7,9,10];
1113 +       my $disk_id = $disk_id{$dev} or next;
1114 +       $sel_sum_H->execute($disk_id,$ino,$size,$mtime,$ctime) or die $!;
1115 +       my($sum4, $dbsum4, $sum5, $dbsum5);
1116 +       my $dbsumcnt = 0;
1117 +       while (my($sum_type, $checksum) = $sel_sum_H->fetchrow_array) {
1118 +           if ($sum_type == 4) {
1119 +               $dbsum4 = $checksum;
1120 +               $dbsumcnt++;
1121 +           } elsif ($sum_type == 5) {
1122 +               $dbsum5 = $checksum;
1123 +               $dbsumcnt++;
1124 +           }
1125 +       }
1126 +       $sel_sum_H->finish;
1127 +
1128 +       next if !$check_opt && $dbsumcnt == 2;
1129 +
1130 +       if (!$check_opt || $dbsumcnt || $verbosity > 2) {
1131 +           if (!open(IN, $fn)) {
1132 +               print STDERR "Unable to read $fn: $!\n";
1133 +               next;
1134 +           }
1135 +
1136 +           while (1) {
1137 +               while (sysread(IN, $_, 64*1024)) {
1138 +                   $md4->add($_);
1139 +                   $md5->add($_);
1140 +               }
1141 +               $sum4 = $md4->digest;
1142 +               $sum5 = $md5->digest;
1143 +               print ' ', unpack('H*', $sum4), ' ', unpack('H*', $sum5) if $verbosity > 2;
1144 +               print " $fn" if $verbosity > 1;
1145 +               my($ino2,$size2,$mtime2,$ctime2) = (stat(IN))[1,7,9,10];
1146 +               last if $ino == $ino2 && $size == $size2 && $mtime == $mtime2 && $ctime == $ctime2;
1147 +               $ino = $ino2;
1148 +               $size = $size2;
1149 +               $mtime = $mtime2;
1150 +               $ctime = $ctime2;
1151 +               sysseek(IN, 0, 0);
1152 +               print " REREADING\n" if $verbosity > 1;
1153 +           }
1154 +
1155 +           close IN;
1156 +       } elsif ($verbosity > 1) {
1157 +           print "_$fn";
1158 +       }
1159 +
1160 +       if ($check_opt) {
1161 +           my $dif;
1162 +           if ($dbsumcnt == 0) {
1163 +               $dif = ' --MISSING--';
1164 +           } else {
1165 +               $dif = '';
1166 +               if (!defined $dbsum4) {
1167 +                   $dif .= ' -NO-MD4-';
1168 +               } elsif ($sum4 ne $dbsum4) {
1169 +                   $dif .= ' -MD4-CHANGED-';
1170 +               }
1171 +               if (!defined $dbsum5) {
1172 +                   $dif .= ' ---NO-MD5---';
1173 +               } elsif ($sum5 ne $dbsum5) {
1174 +                   $dif .= ' -MD5-CHANGED-';
1175 +               }
1176 +               if ($dif eq '') {
1177 +                   print " ====OK====\n" if $verbosity > 1;
1178 +                   next;
1179 +               }
1180 +               $dif =~ s/MD4-CHANGED MD5-//;
1181 +           }
1182 +           if ($verbosity < 2) {
1183 +               print $verbosity ? ' ' : "$reldir/";
1184 +               print $fn;
1185 +           }
1186 +           print $dif, "\n";
1187 +           $exit_code = 1;
1188 +       } else {
1189 +           print "\n" if $verbosity > 1;
1190 +           $rep_sum_H->execute($disk_id, $ino, $size, $mtime, $ctime, 4, $sum4);
1191 +           $rep_sum_H->execute($disk_id, $ino, $size, $mtime, $ctime, 5, $sum5);
1192 +       }
1193 +    }
1194 +
1195 +    closedir DP;
1196 +
1197 +    unshift(@dirs, sort @subdirs) if $recurse_opt;
1198 +}
1199 +
1200 +exit $exit_code;
1201 +
1202 +sub usage
1203 +{
1204 +    die <<EOT;
1205 +Usage: rsyncsums --db=CONFIG_FILE [OPTIONS] [DIRS]
1206 +
1207 +Options:
1208 +     --db=FILE     Specify the config FILE to read for the DB info.
1209 + -m, --mounts      Update mount info.
1210 + -r, --recurse     Scan files in subdirectories too.
1211 + -c, --check       Check if the checksums are right (doesn't update).
1212 + -v, --verbose     Mention what we're doing.  Repeat for more info.
1213 + -h, --help        Display this help message.
1214 +EOT
1215 +}