Adding backup-deleted patch adapted from bug 7889
[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 improvements may include:
7
8  - Updating of MD5 checksums when transferring any file, even w/o -c.
9    We should be able to extend this to work for MD4 checksums too if we
10    make the sender force checksum_seed to 0 when using a DB and having
11    the receiving side check to see if it got a 0 checksum_seed.  (We
12    probably don't want to compute 2 MD4 checksums for the case where
13    the checksum_seed is non-zero.)
14
15  - Caching of path info that allows for the finding of files to use for
16    moving/linking/copying/alternate-basis-use.
17
18  - Extend DB support beyond MySQL and SQLite (PostgreSQL?).
19
20 To use this patch, run these commands for a successful build:
21
22     patch -p1 <patches/db.diff
23     ./configure                               (optional if already run)
24     make
25
26 based-on: 28b519c93b6db30b6520d46f8cd65160213fddd2
27 diff --git a/Makefile.in b/Makefile.in
28 --- a/Makefile.in
29 +++ b/Makefile.in
30 @@ -36,7 +36,7 @@ ZLIBOBJ=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \
31  OBJS1=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
32         util.o main.o checksum.o match.o syscall.o log.o backup.o delete.o
33  OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \
34 -       fileio.o batch.o clientname.o chmod.o acls.o xattrs.o
35 +       fileio.o batch.o clientname.o chmod.o db.o acls.o xattrs.o
36  OBJS3=progress.o pipe.o
37  DAEMON_OBJ = params.o loadparm.o clientserver.o access.o connection.o authenticate.o
38  popt_OBJS=popt/findme.o  popt/popt.o  popt/poptconfig.o \
39 diff --git a/checksum.c b/checksum.c
40 --- a/checksum.c
41 +++ b/checksum.c
42 @@ -23,6 +23,7 @@
43  
44  extern int checksum_seed;
45  extern int protocol_version;
46 +extern int use_db;
47  
48  /*
49    a simple 32 bit checksum that can be upadted from either end
50 @@ -98,10 +99,10 @@ void get_checksum2(char *buf, int32 len, char *sum)
51         }
52  }
53  
54 -void file_checksum(char *fname, char *sum, OFF_T size)
55 +void file_checksum(const char *fname, STRUCT_STAT *st_p, char *sum)
56  {
57         struct map_struct *buf;
58 -       OFF_T i, len = size;
59 +       OFF_T i, len = st_p->st_size;
60         md_context m;
61         int32 remainder;
62         int fd;
63 @@ -112,7 +113,7 @@ void file_checksum(char *fname, char *sum, OFF_T size)
64         if (fd == -1)
65                 return;
66  
67 -       buf = map_file(fd, size, MAX_MAP_SIZE, CSUM_CHUNK);
68 +       buf = map_file(fd, len, MAX_MAP_SIZE, CSUM_CHUNK);
69  
70         if (protocol_version >= 30) {
71                 md5_begin(&m);
72 @@ -146,6 +147,9 @@ void file_checksum(char *fname, char *sum, OFF_T size)
73                 mdfour_result(&m, (uchar *)sum);
74         }
75  
76 +       if (use_db)
77 +               db_set_checksum(fname, st_p, sum);
78 +
79         close(fd);
80         unmap_file(buf);
81  }
82 diff --git a/cleanup.c b/cleanup.c
83 --- a/cleanup.c
84 +++ b/cleanup.c
85 @@ -26,6 +26,7 @@ extern int am_server;
86  extern int am_daemon;
87  extern int am_receiver;
88  extern int io_error;
89 +extern int use_db;
90  extern int keep_partial;
91  extern int got_xfer_error;
92  extern int protocol_version;
93 @@ -142,6 +143,12 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)
94  #include "case_N.h"
95                 switch_step++;
96  
97 +               if (use_db)
98 +                       db_disconnect();
99 +
100 +               /* FALLTHROUGH */
101 +#include "case_N.h"
102 +
103                 if (cleanup_child_pid != -1) {
104                         int status;
105                         int pid = wait_process(cleanup_child_pid, &status, WNOHANG);
106 diff --git a/clientserver.c b/clientserver.c
107 --- a/clientserver.c
108 +++ b/clientserver.c
109 @@ -42,12 +42,15 @@ extern int numeric_ids;
110  extern int filesfrom_fd;
111  extern int remote_protocol;
112  extern int protocol_version;
113 +extern int always_checksum;
114  extern int io_timeout;
115  extern int no_detach;
116 +extern int use_db;
117  extern int write_batch;
118  extern int default_af_hint;
119  extern int logfile_format_has_i;
120  extern int logfile_format_has_o_or_i;
121 +extern char *db_config;
122  extern char *bind_address;
123  extern char *config_file;
124  extern char *logfile_format;
125 @@ -672,6 +675,9 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
126  
127         log_init(1);
128  
129 +       if (*lp_db_config(i))
130 +               db_read_config(FLOG, lp_db_config(i));
131 +
132  #ifdef HAVE_PUTENV
133         if (*lp_prexfer_exec(i) || *lp_postxfer_exec(i)) {
134                 int status;
135 @@ -868,6 +874,10 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
136  
137         am_server = 1; /* Don't let someone try to be tricky. */
138         quiet = 0;
139 +       db_config = NULL;
140 +       if (!always_checksum)
141 +               use_db = 0;
142 +
143         if (lp_ignore_errors(module_id))
144                 ignore_errors = 1;
145         if (write_batch < 0)
146 diff --git a/configure.ac b/configure.ac
147 --- a/configure.ac
148 +++ b/configure.ac
149 @@ -337,7 +337,7 @@ AC_CHECK_HEADERS(sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \
150      sys/un.h sys/attr.h mcheck.h arpa/inet.h arpa/nameser.h locale.h \
151      netdb.h malloc.h float.h limits.h iconv.h libcharset.h langinfo.h \
152      sys/acl.h acl/libacl.h attr/xattr.h sys/xattr.h sys/extattr.h \
153 -    popt.h popt/popt.h)
154 +    popt.h popt/popt.h mysql/mysql.h sqlite3.h)
155  AC_HEADER_MAJOR
156  
157  AC_CACHE_CHECK([if makedev takes 3 args],rsync_cv_MAKEDEV_TAKES_3_ARGS,[
158 @@ -1060,6 +1060,29 @@ if test x"$enable_acl_support" = x"no" -o x"$enable_xattr_support" = x"no" -o x"
159      fi
160  fi
161  
162 +AC_CHECK_PROG(MYSQL_CONFIG, mysql_config, 1, 0)
163 +if test x$MYSQL_CONFIG = x1; then
164 +    AC_MSG_CHECKING(for mysql version >= 4)
165 +    mysql_version=`mysql_config --version`
166 +    mysql_major_version=`echo $mysql_version | sed 's/\..*//'`
167 +    if test $mysql_major_version -lt 4; then
168 +       AC_MSG_RESULT(no.. skipping MySQL)
169 +    else
170 +       AC_MSG_RESULT(yes)
171 +
172 +       MYSQL_CFLAGS=`mysql_config --cflags`
173 +       MYSQL_LIBS=`mysql_config --libs`
174 +
175 +       CPPFLAGS="$CPPFLAGS $MYSQL_CFLAGS"
176 +       LIBS="$MYSQL_LIBS $LIBS"
177 +
178 +       AC_CHECK_LIB(mysqlclient, mysql_init)
179 +   fi
180 +fi
181 +
182 +AC_CHECK_LIB(sqlite3, sqlite3_open)
183 +AC_CHECK_FUNCS(sqlite3_open_v2 sqlite3_prepare_v2)
184 +
185  case "$CC" in
186  ' checker'*|checker*)
187      AC_DEFINE(FORCE_FD_ZERO_MEMSET, 1, [Used to make "checker" understand that FD_ZERO() clears memory.])
188 diff --git a/db.c b/db.c
189 new file mode 100644
190 --- /dev/null
191 +++ b/db.c
192 @@ -0,0 +1,567 @@
193 +/*
194 + * Routines to access extended file info via DB.
195 + *
196 + * Copyright (C) 2008 Wayne Davison
197 + *
198 + * This program is free software; you can redistribute it and/or modify
199 + * it under the terms of the GNU General Public License as published by
200 + * the Free Software Foundation; either version 3 of the License, or
201 + * (at your option) any later version.
202 + *
203 + * This program is distributed in the hope that it will be useful,
204 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
205 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
206 + * GNU General Public License for more details.
207 + *
208 + * You should have received a copy of the GNU General Public License along
209 + * with this program; if not, visit the http://fsf.org website.
210 + */
211 +
212 +#include "rsync.h"
213 +#include "ifuncs.h"
214 +#include "itypes.h"
215 +
216 +#if defined HAVE_MYSQL_MYSQL_H && defined HAVE_LIBMYSQLCLIENT
217 +#define USE_MYSQL
218 +#include <mysql/mysql.h>
219 +#include <mysql/errmsg.h>
220 +#endif
221 +
222 +#if defined HAVE_SQLITE3_H && defined HAVE_LIBSQLITE3
223 +#define USE_SQLITE
224 +#include <sqlite3.h>
225 +#ifndef HAVE_SQLITE3_OPEN_V2
226 +#define sqlite3_open_v2(dbname, dbhptr, flags, vfs) \
227 +       sqlite3_open(dbname, dbhptr)
228 +#endif
229 +#ifndef HAVE_SQLITE3_PREPARE_V2
230 +#define sqlite3_prepare_v2 sqlite3_prepare
231 +#endif
232 +#endif
233 +
234 +extern int protocol_version;
235 +extern int checksum_len;
236 +
237 +#define DB_TYPE_NONE 0
238 +#define DB_TYPE_MYSQL 1
239 +#define DB_TYPE_SQLITE 2
240 +
241 +int use_db = DB_TYPE_NONE;
242 +
243 +static const char *dbhost = NULL, *dbuser = NULL, *dbpass = NULL, *dbname = NULL;
244 +static unsigned int dbport = 0;
245 +
246 +static union {
247 +#ifdef USE_MYSQL
248 +    MYSQL *mysql;
249 +#endif
250 +#ifdef USE_SQLITE
251 +    sqlite3 *sqlite;
252 +#endif
253 +    void *all;
254 +} dbh;
255 +
256 +#define SEL_DEV 0
257 +#define SEL_SUM 1
258 +#define REP_SUM 2
259 +#define MAX_PREP_CNT 3
260 +
261 +static union {
262 +#ifdef USE_MYSQL
263 +    MYSQL_STMT *mysql;
264 +#endif
265 +#ifdef USE_SQLITE
266 +    sqlite3_stmt *sqlite;
267 +#endif
268 +    void *all;
269 +} statements[MAX_PREP_CNT];
270 +
271 +static int md_num;
272 +static enum logcode log_code;
273 +
274 +#ifdef USE_MYSQL
275 +static unsigned int bind_disk_id;
276 +static unsigned long long bind_devno, bind_ino, bind_size, bind_mtime, bind_ctime;
277 +static char bind_sum[MAX_DIGEST_LEN];
278 +#endif
279 +static char bind_thishost[256];
280 +static int bind_thishost_len;
281 +
282 +static unsigned int prior_disk_id = 0;
283 +static unsigned long long prior_devno = 0;
284 +
285 +int db_read_config(enum logcode code, const char *config_file)
286 +{
287 +       char buf[2048], *cp;
288 +       FILE *fp;
289 +       int lineno = 0;
290 +
291 +       log_code = code;
292 +
293 +       bind_thishost_len = strlcpy(bind_thishost, "localhost", sizeof bind_thishost);
294 +
295 +       if (!(fp = fopen(config_file, "r"))) {
296 +               rsyserr(log_code, errno, "unable to open %s", config_file);
297 +               return 0;
298 +       }
299 +       while (fgets(buf, sizeof buf, fp)) {
300 +               lineno++;
301 +               if ((cp = strchr(buf, '#')) == NULL
302 +                && (cp = strchr(buf, '\r')) == NULL
303 +                && (cp = strchr(buf, '\n')) == NULL)
304 +                       cp = buf + strlen(buf);
305 +               while (cp != buf && isSpace(cp-1)) cp--;
306 +               *cp = '\0';
307 +
308 +               if (!*buf)
309 +                       continue;
310 +
311 +               if (!(cp = strchr(buf, ':')))
312 +                       goto invalid_line;
313 +               *cp++ = '\0';
314 +
315 +               while (isSpace(cp)) cp++;
316 +               if (strcasecmp(buf, "dbhost") == 0)
317 +                       dbhost = strdup(cp);
318 +               else if (strcasecmp(buf, "dbuser") == 0)
319 +                       dbuser = strdup(cp);
320 +               else if (strcasecmp(buf, "dbpass") == 0)
321 +                       dbpass = strdup(cp);
322 +               else if (strcasecmp(buf, "dbname") == 0)
323 +                       dbname = strdup(cp);
324 +               else if (strcasecmp(buf, "dbport") == 0)
325 +                       dbport = atoi(cp);
326 +               else if (strcasecmp(buf, "thishost") == 0)
327 +                       bind_thishost_len = strlcpy(bind_thishost, cp, sizeof bind_thishost);
328 +               else if (strcasecmp(buf, "dbtype") == 0) {
329 +#ifdef USE_MYSQL
330 +                       if (strcasecmp(cp, "mysql") == 0) {
331 +                               use_db = DB_TYPE_MYSQL;
332 +                               continue;
333 +                       }
334 +#endif
335 +#ifdef USE_SQLITE
336 +                       if (strcasecmp(cp, "sqlite") == 0) {
337 +                               use_db = DB_TYPE_SQLITE;
338 +                               continue;
339 +                       }
340 +#endif
341 +                       rprintf(log_code,
342 +                           "Unsupported dbtype on line #%d in %s.\n",
343 +                           lineno, config_file);
344 +                       use_db = DB_TYPE_NONE;
345 +                       return 0;
346 +               } else {
347 +                 invalid_line:
348 +                       rprintf(log_code, "Invalid line #%d in %s\n",
349 +                               lineno, config_file);
350 +                       use_db = DB_TYPE_NONE;
351 +                       return 0;
352 +               }
353 +       }
354 +       fclose(fp);
355 +
356 +       if (bind_thishost_len >= (int)sizeof bind_thishost)
357 +               bind_thishost_len = sizeof bind_thishost - 1;
358 +
359 +       if (!use_db || !dbname) {
360 +               rprintf(log_code, "Please specify at least dbtype and dbname in %s.\n", config_file);
361 +               use_db = DB_TYPE_NONE;
362 +               return 0;
363 +       }
364 +
365 +       md_num = protocol_version >= 30 ? 5 : 4;
366 +
367 +       return 1;
368 +}
369 +
370 +#ifdef USE_MYSQL
371 +static MYSQL_STMT *prepare_mysql(MYSQL_BIND *binds, int bind_cnt, const char *fmt, ...)
372 +{
373 +       va_list ap;
374 +       char *query;
375 +       int qlen, param_cnt;
376 +       MYSQL_STMT *stmt = mysql_stmt_init(dbh.mysql);
377 +
378 +       if (stmt == NULL)
379 +               out_of_memory("prepare_mysql");
380 +
381 +       va_start(ap, fmt);
382 +       qlen = vasprintf(&query, fmt, ap);
383 +       va_end(ap);
384 +       if (qlen < 0)
385 +               out_of_memory("prepare_mysql");
386 +
387 +       if (mysql_stmt_prepare(stmt, query, qlen) != 0) {
388 +               rprintf(log_code, "Prepare failed: %s\n", mysql_stmt_error(stmt));
389 +               return NULL;
390 +       }
391 +       free(query);
392 +
393 +       if ((param_cnt = mysql_stmt_param_count(stmt)) != bind_cnt) {
394 +               rprintf(log_code, "Parameters in statement = %d, bind vars = %d\n",
395 +                       param_cnt, bind_cnt);
396 +               return NULL;
397 +       }
398 +       if (bind_cnt)
399 +               mysql_stmt_bind_param(stmt, binds);
400 +
401 +       return stmt;
402 +}
403 +#endif
404 +
405 +#ifdef USE_MYSQL
406 +static int db_connect_mysql(void)
407 +{
408 +       MYSQL_BIND binds[10];
409 +
410 +       if (!(dbh.mysql = mysql_init(NULL)))
411 +               out_of_memory("db_read_config");
412 +
413 +       if (!mysql_real_connect(dbh.mysql, dbhost, dbuser, dbpass, dbname, dbport, NULL, 0))
414 +               return 0;
415 +
416 +       memset(binds, 0, sizeof binds);
417 +       binds[0].buffer_type = MYSQL_TYPE_LONGLONG;
418 +       binds[0].buffer = &bind_devno;
419 +       binds[1].buffer_type = MYSQL_TYPE_STRING;
420 +       binds[1].buffer = &bind_thishost;
421 +       binds[1].buffer_length = bind_thishost_len;
422 +       statements[SEL_DEV].mysql = prepare_mysql(binds, 2,
423 +               "SELECT disk_id"
424 +               " FROM disk"
425 +               " WHERE devno = ? AND host = ? AND mounted = 1");
426 +       if (!statements[SEL_DEV].mysql)
427 +               return 0;
428 +
429 +       memset(binds, 0, sizeof binds);
430 +       binds[0].buffer_type = MYSQL_TYPE_LONG;
431 +       binds[0].buffer = &bind_disk_id;
432 +       binds[1].buffer_type = MYSQL_TYPE_LONGLONG;
433 +       binds[1].buffer = &bind_ino;
434 +       binds[2].buffer_type = MYSQL_TYPE_LONGLONG;
435 +       binds[2].buffer = &bind_size;
436 +       binds[3].buffer_type = MYSQL_TYPE_LONGLONG;
437 +       binds[3].buffer = &bind_mtime;
438 +       binds[4].buffer_type = MYSQL_TYPE_LONGLONG;
439 +       binds[4].buffer = &bind_ctime;
440 +       statements[SEL_SUM].mysql = prepare_mysql(binds, 5,
441 +               "SELECT checksum"
442 +               " FROM inode_map"
443 +               " WHERE disk_id = ? AND ino = ? AND sum_type = %d"
444 +               "   AND size = ? AND mtime = ? AND ctime = ?",
445 +               md_num);
446 +       if (!statements[SEL_SUM].mysql)
447 +               return 0;
448 +
449 +       memset(binds, 0, sizeof binds);
450 +       binds[0].buffer_type = MYSQL_TYPE_LONG;
451 +       binds[0].buffer = &bind_disk_id;
452 +       binds[1].buffer_type = MYSQL_TYPE_LONGLONG;
453 +       binds[1].buffer = &bind_ino;
454 +       binds[2].buffer_type = binds[6].buffer_type = MYSQL_TYPE_LONGLONG;
455 +       binds[2].buffer = binds[6].buffer = &bind_size;
456 +       binds[3].buffer_type = binds[7].buffer_type = MYSQL_TYPE_LONGLONG;
457 +       binds[3].buffer = binds[7].buffer = &bind_mtime;
458 +       binds[4].buffer_type = binds[8].buffer_type = MYSQL_TYPE_LONGLONG;
459 +       binds[4].buffer = binds[8].buffer = &bind_ctime;
460 +       binds[5].buffer_type = binds[9].buffer_type = MYSQL_TYPE_BLOB;
461 +       binds[5].buffer = binds[9].buffer = &bind_sum;
462 +       binds[5].buffer_length = binds[9].buffer_length = checksum_len;
463 +       statements[REP_SUM].mysql = prepare_mysql(binds, 10,
464 +               "INSERT INTO inode_map"
465 +               " SET disk_id = ?, ino = ?, sum_type = %d,"
466 +               "     size = ?, mtime = ?, ctime = ?, checksum = ?"
467 +               " ON DUPLICATE KEY"
468 +               " UPDATE size = ?, mtime = ?, ctime = ?, checksum = ?",
469 +               md_num, md_num);
470 +       if (!statements[REP_SUM].mysql)
471 +               return 0;
472 +
473 +       return 1;
474 +}
475 +#endif
476 +
477 +#ifdef USE_SQLITE
478 +static int db_connect_sqlite(void)
479 +{
480 +       char *sql;
481 +
482 +       if (sqlite3_open_v2(dbname, &dbh.sqlite, SQLITE_OPEN_READWRITE, NULL) != 0)
483 +               return 0;
484 +
485 +       sql = "SELECT disk_id"
486 +           " FROM disk"
487 +           " WHERE devno = ? AND host = ? AND mounted = 1";
488 +       if (sqlite3_prepare_v2(dbh.sqlite, sql, -1, &statements[SEL_DEV].sqlite, NULL) != 0)
489 +               return 0;
490 +
491 +       if (asprintf(&sql,
492 +           "SELECT checksum"
493 +           " FROM inode_map"
494 +           " WHERE disk_id = ? AND ino = ? AND sum_type = %d"
495 +           "   AND size = ? AND mtime = ? AND ctime = ?",
496 +           md_num) < 0
497 +        || sqlite3_prepare_v2(dbh.sqlite, sql, -1, &statements[SEL_SUM].sqlite, NULL) != 0)
498 +               return 0;
499 +       free(sql);
500 +
501 +       if (asprintf(&sql,
502 +           "INSERT OR REPLACE INTO inode_map"
503 +           " (disk_id, ino, sum_type, size, mtime, ctime, checksum)"
504 +           " VALUES(?, ?, %d, ?, ?, ?, ?)",
505 +           md_num) < 0
506 +        || sqlite3_prepare_v2(dbh.sqlite, sql, -1, &statements[REP_SUM].sqlite, NULL) != 0)
507 +               return 0;
508 +       free(sql);
509 +
510 +       return 1;
511 +}
512 +#endif
513 +
514 +int db_connect(void)
515 +{
516 +       switch (use_db) {
517 +#ifdef USE_MYSQL
518 +       case DB_TYPE_MYSQL:
519 +               if (db_connect_mysql())
520 +                       return 1;
521 +               break;
522 +#endif
523 +#ifdef USE_SQLITE
524 +       case DB_TYPE_SQLITE:
525 +               if (db_connect_sqlite())
526 +                       return 1;
527 +               break;
528 +#endif
529 +       }
530 +
531 +       rprintf(log_code, "Unable to connect to DB\n");
532 +       db_disconnect();
533 +       use_db = DB_TYPE_NONE;
534 +
535 +       return 0;
536 +}
537 +
538 +void db_disconnect(void)
539 +{
540 +       int ndx;
541 +
542 +       if (!dbh.all)
543 +               return;
544 +
545 +       for (ndx = 0; ndx < MAX_PREP_CNT; ndx++) {
546 +               if (statements[ndx].all) {
547 +                       switch (use_db) {
548 +#ifdef USE_MYSQL
549 +                       case DB_TYPE_MYSQL:
550 +                               mysql_stmt_close(statements[ndx].mysql);
551 +                               break;
552 +#endif
553 +#ifdef USE_SQLITE
554 +                       case DB_TYPE_SQLITE:
555 +                               sqlite3_finalize(statements[ndx].sqlite);
556 +                               break;
557 +#endif
558 +                       }
559 +                       statements[ndx].all = NULL;
560 +               }
561 +       }
562 +
563 +       switch (use_db) {
564 +#ifdef USE_MYSQL
565 +       case DB_TYPE_MYSQL:
566 +               mysql_close(dbh.mysql);
567 +               break;
568 +#endif
569 +#ifdef USE_SQLITE
570 +       case DB_TYPE_SQLITE:
571 +               sqlite3_close(dbh.sqlite);
572 +               break;
573 +#endif
574 +       }
575 +
576 +       dbh.all = NULL;
577 +}
578 +
579 +#ifdef USE_MYSQL
580 +static MYSQL_STMT *exec_mysql(int ndx)
581 +{
582 +       MYSQL_STMT *stmt = statements[ndx].mysql;
583 +       int rc;
584 +
585 +       if ((rc = mysql_stmt_execute(stmt)) == CR_SERVER_LOST) {
586 +               db_disconnect();
587 +               if (db_connect()) {
588 +                       stmt = statements[ndx].mysql;
589 +                       rc = mysql_stmt_execute(stmt);
590 +               }
591 +       }
592 +       if (rc != 0) {
593 +               rprintf(log_code, "SQL execute failed: %s\n", mysql_stmt_error(stmt));
594 +               return NULL;
595 +       }
596 +
597 +       return stmt;
598 +}
599 +#endif
600 +
601 +#ifdef USE_MYSQL
602 +static int fetch_mysql(MYSQL_BIND *binds, int bind_cnt, int ndx)
603 +{
604 +       unsigned long length[32];
605 +       my_bool is_null[32], error[32];
606 +       MYSQL_STMT *stmt;
607 +       int i, rc;
608 +
609 +       if (bind_cnt > 32)
610 +               exit_cleanup(RERR_UNSUPPORTED);
611 +
612 +       if ((stmt = exec_mysql(ndx)) == NULL)
613 +               return 0;
614 +
615 +       for (i = 0; i < bind_cnt; i++) {
616 +               binds[i].is_null = &is_null[i];
617 +               binds[i].length = &length[i];
618 +               binds[i].error = &error[i];
619 +       }
620 +       mysql_stmt_bind_result(stmt, binds);
621 +
622 +       if ((rc = mysql_stmt_fetch(stmt)) != 0) {
623 +               if (rc != MYSQL_NO_DATA) {
624 +                       rprintf(log_code, "SELECT fetch failed: %s\n",
625 +                               mysql_stmt_error(stmt));
626 +               }
627 +               mysql_stmt_free_result(stmt);
628 +               return 0;
629 +       }
630 +
631 +       mysql_stmt_free_result(stmt);
632 +
633 +       return is_null[0] ? 0 : 1;
634 +}
635 +#endif
636 +
637 +static void get_disk_id(unsigned long long devno)
638 +{
639 +       switch (use_db) {
640 +#ifdef USE_MYSQL
641 +       case DB_TYPE_MYSQL: {
642 +               MYSQL_BIND binds[1];
643 +
644 +               bind_devno = devno; /* The one variable SEL_DEV input value. */
645 +
646 +               /* Bind where to put the output. */
647 +               binds[0].buffer_type = MYSQL_TYPE_LONG;
648 +               binds[0].buffer = &prior_disk_id;
649 +               if (!fetch_mysql(binds, 1, SEL_DEV))
650 +                       prior_disk_id = 0;
651 +               break;
652 +           }
653 +#endif
654 +#ifdef USE_SQLITE
655 +       case DB_TYPE_SQLITE: {
656 +               sqlite3_stmt *stmt = statements[SEL_DEV].sqlite;
657 +               sqlite3_bind_int64(stmt, 1, devno);
658 +               sqlite3_bind_text(stmt, 2, bind_thishost, bind_thishost_len, SQLITE_STATIC);
659 +               if (sqlite3_step(stmt) == SQLITE_ROW)
660 +                       prior_disk_id = sqlite3_column_int(stmt, 0);
661 +               else
662 +                       prior_disk_id = 0;
663 +               sqlite3_reset(stmt);
664 +               break;
665 +           }
666 +#endif
667 +       }
668 +
669 +       prior_devno = devno;
670 +}
671 +
672 +int db_get_checksum(UNUSED(const char *fname), const STRUCT_STAT *st_p, char *sum)
673 +{
674 +       if (prior_devno != st_p->st_dev)
675 +               get_disk_id(st_p->st_dev);
676 +       if (prior_disk_id == 0)
677 +               return 0;
678 +
679 +       switch (use_db) {
680 +#ifdef USE_MYSQL
681 +       case DB_TYPE_MYSQL: {
682 +               MYSQL_BIND binds[1];
683 +
684 +               bind_disk_id = prior_disk_id;
685 +               bind_ino = st_p->st_ino;
686 +               bind_size = st_p->st_size;
687 +               bind_mtime = st_p->st_mtime;
688 +               bind_ctime = st_p->st_ctime;
689 +
690 +               binds[0].buffer_type = MYSQL_TYPE_BLOB;
691 +               binds[0].buffer = sum;
692 +               binds[0].buffer_length = checksum_len;
693 +               return fetch_mysql(binds, 1, SEL_SUM);
694 +           }
695 +#endif
696 +#ifdef USE_SQLITE
697 +       case DB_TYPE_SQLITE: {
698 +               sqlite3_stmt *stmt = statements[SEL_SUM].sqlite;
699 +               sqlite3_bind_int(stmt, 1, prior_disk_id);
700 +               sqlite3_bind_int64(stmt, 2, st_p->st_ino);
701 +               sqlite3_bind_int64(stmt, 3, st_p->st_size);
702 +               sqlite3_bind_int64(stmt, 4, st_p->st_mtime);
703 +               sqlite3_bind_int64(stmt, 5, st_p->st_ctime);
704 +               if (sqlite3_step(stmt) == SQLITE_ROW) {
705 +                       int len = sqlite3_column_bytes(stmt, 0);
706 +                       if (len > MAX_DIGEST_LEN)
707 +                               len = MAX_DIGEST_LEN;
708 +                       memcpy(sum, sqlite3_column_blob(stmt, 0), len);
709 +                       sqlite3_reset(stmt);
710 +                       return 1;
711 +               }
712 +               sqlite3_reset(stmt);
713 +               return 0;
714 +           }
715 +#endif
716 +       }
717 +
718 +       return 0;
719 +}
720 +
721 +int db_set_checksum(UNUSED(const char *fname), const STRUCT_STAT *st_p, const char *sum)
722 +{
723 +       if (prior_devno != st_p->st_dev)
724 +               get_disk_id(st_p->st_dev);
725 +       if (prior_disk_id == 0)
726 +               return 0;
727 +
728 +       switch (use_db) {
729 +#ifdef USE_MYSQL
730 +       case DB_TYPE_MYSQL: {
731 +               bind_disk_id = prior_disk_id;
732 +               bind_ino = st_p->st_ino;
733 +               bind_size = st_p->st_size;
734 +               bind_mtime = st_p->st_mtime;
735 +               bind_ctime = st_p->st_ctime;
736 +               memcpy(bind_sum, sum, checksum_len);
737 +
738 +               return exec_mysql(REP_SUM) != NULL;
739 +           }
740 +#endif
741 +#ifdef USE_SQLITE
742 +       case DB_TYPE_SQLITE: {
743 +               int rc;
744 +               sqlite3_stmt *stmt = statements[REP_SUM].sqlite;
745 +               sqlite3_bind_int(stmt, 1, prior_disk_id);
746 +               sqlite3_bind_int64(stmt, 2, st_p->st_ino);
747 +               sqlite3_bind_int64(stmt, 3, st_p->st_size);
748 +               sqlite3_bind_int64(stmt, 4, st_p->st_mtime);
749 +               sqlite3_bind_int64(stmt, 5, st_p->st_ctime);
750 +               sqlite3_bind_blob(stmt, 6, sum, checksum_len, SQLITE_TRANSIENT);
751 +               rc = sqlite3_step(stmt);
752 +               sqlite3_reset(stmt);
753 +               return rc == SQLITE_DONE;
754 +           }
755 +#endif
756 +       }
757 +
758 +       return 0;
759 +}
760 diff --git a/flist.c b/flist.c
761 --- a/flist.c
762 +++ b/flist.c
763 @@ -52,6 +52,7 @@ extern int preserve_devices;
764  extern int preserve_specials;
765  extern int delete_during;
766  extern int missing_args;
767 +extern int use_db;
768  extern int eol_nulls;
769  extern int relative_paths;
770  extern int implied_dirs;
771 @@ -1309,11 +1310,8 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
772                 extra_len += EXTRA_LEN;
773  #endif
774  
775 -       if (always_checksum && am_sender && S_ISREG(st.st_mode)) {
776 -               file_checksum(thisname, tmp_sum, st.st_size);
777 -               if (sender_keeps_checksum)
778 -                       extra_len += SUM_EXTRA_CNT * EXTRA_LEN;
779 -       }
780 +       if (sender_keeps_checksum && S_ISREG(st.st_mode))
781 +               extra_len += SUM_EXTRA_CNT * EXTRA_LEN;
782  
783  #if EXTRA_ROUNDING > 0
784         if (extra_len & (EXTRA_ROUNDING * EXTRA_LEN))
785 @@ -1398,8 +1396,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
786                 return NULL;
787         }
788  
789 -       if (sender_keeps_checksum && S_ISREG(st.st_mode))
790 -               memcpy(F_SUM(file), tmp_sum, checksum_len);
791 +       if (always_checksum && am_sender && S_ISREG(st.st_mode)) {
792 +               if (!use_db || !db_get_checksum(thisname, &st, tmp_sum))
793 +                       file_checksum(thisname, &st, tmp_sum);
794 +               if (sender_keeps_checksum)
795 +                       memcpy(F_SUM(file), tmp_sum, checksum_len);
796 +       }
797  
798         if (unsort_ndx)
799                 F_NDX(file) = stats.num_dirs;
800 @@ -2080,6 +2082,9 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
801                      | (eol_nulls || reading_remotely ? RL_EOL_NULLS : 0);
802         int implied_dot_dir = 0;
803  
804 +       if (use_db)
805 +               db_connect();
806 +
807         rprintf(FLOG, "building file list\n");
808         if (show_filelist_p())
809                 start_filelist_progress("building file list");
810 diff --git a/generator.c b/generator.c
811 --- a/generator.c
812 +++ b/generator.c
813 @@ -58,6 +58,7 @@ extern int human_readable;
814  extern int ignore_existing;
815  extern int ignore_non_existing;
816  extern int inplace;
817 +extern int use_db;
818  extern int append_mode;
819  extern int make_backups;
820  extern int csum_length;
821 @@ -573,7 +574,8 @@ int unchanged_file(char *fn, struct file_struct *file, STRUCT_STAT *st)
822            of the file time to determine whether to sync */
823         if (always_checksum > 0 && S_ISREG(st->st_mode)) {
824                 char sum[MAX_DIGEST_LEN];
825 -               file_checksum(fn, sum, st->st_size);
826 +               if (!use_db || !db_get_checksum(fn, st, sum))
827 +                       file_checksum(fn, st, sum);
828                 return memcmp(sum, F_SUM(file), checksum_len) == 0;
829         }
830  
831 @@ -2138,6 +2140,9 @@ void generate_files(int f_out, const char *local_name)
832                         : "enabled");
833         }
834  
835 +       if (use_db && always_checksum)
836 +               db_connect();
837 +
838         dflt_perms = (ACCESSPERMS & ~orig_umask);
839  
840         do {
841 diff --git a/loadparm.c b/loadparm.c
842 --- a/loadparm.c
843 +++ b/loadparm.c
844 @@ -109,6 +109,7 @@ typedef struct {
845         char *auth_users;
846         char *charset;
847         char *comment;
848 +       char *db_config;
849         char *dont_compress;
850         char *exclude;
851         char *exclude_from;
852 @@ -185,6 +186,7 @@ static const all_vars Defaults = {
853   /* auth_users; */             NULL,
854   /* charset; */                NULL,
855   /* comment; */                NULL,
856 + /* db_config; */              NULL,
857   /* dont_compress; */          DEFAULT_DONT_COMPRESS,
858   /* exclude; */                        NULL,
859   /* exclude_from; */           NULL,
860 @@ -322,6 +324,7 @@ static struct parm_struct parm_table[] =
861   {"auth users",        P_STRING, P_LOCAL, &Vars.l.auth_users,          NULL,0},
862   {"charset",           P_STRING, P_LOCAL, &Vars.l.charset,             NULL,0},
863   {"comment",           P_STRING, P_LOCAL, &Vars.l.comment,             NULL,0},
864 + {"db config",         P_STRING, P_LOCAL, &Vars.l.db_config,           NULL,0},
865   {"dont compress",     P_STRING, P_LOCAL, &Vars.l.dont_compress,       NULL,0},
866   {"exclude from",      P_STRING, P_LOCAL, &Vars.l.exclude_from,        NULL,0},
867   {"exclude",           P_STRING, P_LOCAL, &Vars.l.exclude,             NULL,0},
868 @@ -454,6 +457,7 @@ FN_GLOBAL_INTEGER(lp_rsync_port, &Vars.g.rsync_port)
869  FN_LOCAL_STRING(lp_auth_users, auth_users)
870  FN_LOCAL_STRING(lp_charset, charset)
871  FN_LOCAL_STRING(lp_comment, comment)
872 +FN_LOCAL_STRING(lp_db_config, db_config)
873  FN_LOCAL_STRING(lp_dont_compress, dont_compress)
874  FN_LOCAL_STRING(lp_exclude, exclude)
875  FN_LOCAL_STRING(lp_exclude_from, exclude_from)
876 diff --git a/main.c b/main.c
877 --- a/main.c
878 +++ b/main.c
879 @@ -51,6 +51,7 @@ extern int copy_unsafe_links;
880  extern int keep_dirlinks;
881  extern int preserve_hard_links;
882  extern int protocol_version;
883 +extern int always_checksum;
884  extern int file_total;
885  extern int recurse;
886  extern int xfer_dirs;
887 @@ -83,6 +84,7 @@ extern char *filesfrom_host;
888  extern char *partial_dir;
889  extern char *dest_option;
890  extern char *rsync_path;
891 +extern char *db_config;
892  extern char *shell_cmd;
893  extern char *batch_name;
894  extern char *password_file;
895 @@ -1615,6 +1617,9 @@ int main(int argc,char *argv[])
896                 exit_cleanup(RERR_SYNTAX);
897         }
898  
899 +       if (db_config && always_checksum)
900 +               db_read_config(FERROR, db_config);
901 +
902         if (am_server) {
903                 set_nonblocking(STDIN_FILENO);
904                 set_nonblocking(STDOUT_FILENO);
905 diff --git a/options.c b/options.c
906 --- a/options.c
907 +++ b/options.c
908 @@ -93,6 +93,7 @@ int use_qsort = 0;
909  char *files_from = NULL;
910  int filesfrom_fd = -1;
911  char *filesfrom_host = NULL;
912 +char *db_config = NULL;
913  int eol_nulls = 0;
914  int protect_args = -1;
915  int human_readable = 1;
916 @@ -570,6 +571,7 @@ static void print_rsync_version(enum logcode f)
917         char const *links = "no ";
918         char const *iconv = "no ";
919         char const *ipv6 = "no ";
920 +       char const *db = "no ";
921         STRUCT_STAT *dumstat;
922  
923  #if SUBPROTOCOL_VERSION != 0
924 @@ -606,6 +608,11 @@ static void print_rsync_version(enum logcode f)
925  #ifdef CAN_SET_SYMLINK_TIMES
926         symtimes = "";
927  #endif
928 +#if defined HAVE_MYSQL_MYSQL_H && defined HAVE_LIBMYSQLCLIENT
929 +       db = "";
930 +#elif defined HAVE_SQLITE3_H && defined HAVE_LIBSQLITE3
931 +       db = "";
932 +#endif
933  
934         rprintf(f, "%s  version %s  protocol version %d%s\n",
935                 RSYNC_NAME, RSYNC_VERSION, PROTOCOL_VERSION, subprotocol);
936 @@ -619,8 +626,8 @@ static void print_rsync_version(enum logcode f)
937                 (int)(sizeof (int64) * 8));
938         rprintf(f, "    %ssocketpairs, %shardlinks, %ssymlinks, %sIPv6, batchfiles, %sinplace,\n",
939                 got_socketpair, hardlinks, links, ipv6, have_inplace);
940 -       rprintf(f, "    %sappend, %sACLs, %sxattrs, %siconv, %ssymtimes, %sprealloc\n",
941 -               have_inplace, acls, xattrs, iconv, symtimes, prealloc);
942 +       rprintf(f, "    %sappend, %sACLs, %sxattrs, %siconv, %ssymtimes, %sprealloc, %sdb\n",
943 +               have_inplace, acls, xattrs, iconv, symtimes, prealloc, db);
944  
945  #ifdef MAINTAINER_MODE
946         rprintf(f, "Panic Action: \"%s\"\n", get_panic_action());
947 @@ -668,6 +675,7 @@ void usage(enum logcode F)
948    rprintf(F," -q, --quiet                 suppress non-error messages\n");
949    rprintf(F,"     --no-motd               suppress daemon-mode MOTD (see manpage caveat)\n");
950    rprintf(F," -c, --checksum              skip based on checksum, not mod-time & size\n");
951 +  rprintf(F,"     --db=CONFIG_FILE        specify a CONFIG_FILE for DB checksums\n");
952    rprintf(F," -a, --archive               archive mode; equals -rlptgoD (no -H,-A,-X)\n");
953    rprintf(F,"     --no-OPTION             turn off an implied OPTION (e.g. --no-D)\n");
954    rprintf(F," -r, --recursive             recurse into directories\n");
955 @@ -950,6 +958,7 @@ static struct poptOption long_options[] = {
956    {"checksum",        'c', POPT_ARG_VAL,    &always_checksum, 1, 0, 0 },
957    {"no-checksum",      0,  POPT_ARG_VAL,    &always_checksum, 0, 0, 0 },
958    {"no-c",             0,  POPT_ARG_VAL,    &always_checksum, 0, 0, 0 },
959 +  {"db",               0,  POPT_ARG_STRING, &db_config, 0, 0, 0 },
960    {"block-size",      'B', POPT_ARG_LONG,   &block_size, 0, 0, 0 },
961    {"compare-dest",     0,  POPT_ARG_STRING, 0, OPT_COMPARE_DEST, 0, 0 },
962    {"copy-dest",        0,  POPT_ARG_STRING, 0, OPT_COPY_DEST, 0, 0 },
963 diff --git a/pipe.c b/pipe.c
964 --- a/pipe.c
965 +++ b/pipe.c
966 @@ -27,6 +27,9 @@ extern int am_server;
967  extern int blocking_io;
968  extern int filesfrom_fd;
969  extern int munge_symlinks;
970 +extern int always_checksum;
971 +extern int use_db;
972 +extern char *db_config;
973  extern char *logfile_name;
974  extern int remote_option_cnt;
975  extern const char **remote_options;
976 @@ -141,6 +144,9 @@ pid_t local_child(int argc, char **argv, int *f_in, int *f_out,
977                         logfile_close();
978                 }
979  
980 +               use_db = 0;
981 +               db_config = NULL;
982 +
983                 if (remote_option_cnt) {
984                         int rc = remote_option_cnt + 1;
985                         const char **rv = remote_options;
986 @@ -148,6 +154,8 @@ pid_t local_child(int argc, char **argv, int *f_in, int *f_out,
987                                 option_error();
988                                 exit_cleanup(RERR_SYNTAX);
989                         }
990 +                       if (db_config && always_checksum)
991 +                               db_read_config(FERROR, db_config);
992                 }
993  
994                 if (dup2(to_child_pipe[0], STDIN_FILENO) < 0 ||
995 diff --git a/rsync.yo b/rsync.yo
996 --- a/rsync.yo
997 +++ b/rsync.yo
998 @@ -323,6 +323,7 @@ to the detailed description below for a complete description.  verb(
999   -q, --quiet                 suppress non-error messages
1000       --no-motd               suppress daemon-mode MOTD (see caveat)
1001   -c, --checksum              skip based on checksum, not mod-time & size
1002 +     --db=CONFIG_FILE        specify a CONFIG_FILE for DB checksums
1003   -a, --archive               archive mode; equals -rlptgoD (no -H,-A,-X)
1004       --no-OPTION             turn off an implied OPTION (e.g. --no-D)
1005   -r, --recursive             recurse into directories
1006 @@ -589,6 +590,47 @@ option's before-the-transfer "Does this file need to be updated?" check.
1007  For protocol 30 and beyond (first supported in 3.0.0), the checksum used is
1008  MD5.  For older protocols, the checksum used is MD4.
1009  
1010 +dit(bf(--db=CONFIG_FILE))  This option specifies a CONFIG_FILE to read
1011 +that holds connection details for a database of checksum information.
1012 +When combined with the bf(--checksum) (bf(-c)) option, rsync will try to
1013 +use cached checksum information from the DB, and will update it if it is
1014 +missing.
1015 +
1016 +The currently supported DB choices are MySQL and SQLite.  For example, a
1017 +MySQL configuration might look like this:
1018 +
1019 +verb(    dbtype: mysql
1020 +    dbhost: 127.0.0.1
1021 +    dbname: rsyncdb
1022 +    dbuser: rsyncuser
1023 +    dbpass: somepass
1024 +    port: 3306
1025 +    thishost: hostname )
1026 +
1027 +And a SQLite configuration might look like this:
1028 +
1029 +verb(    dbtype: SQLite
1030 +    dbname: /var/cache/rsync/sum.db )
1031 +
1032 +This option only affects one side of a transfer.  See the
1033 +bf(--remote-option) option for a way to specify the option for both
1034 +sides of the transfer (with each side reading the config file from
1035 +their local filesystem).  For example:
1036 +
1037 +verb(    rsync -avc {-M,}--db=/etc/rsyncdb.conf src/ host:dest/ )
1038 +
1039 +See the perl script "rsyncdb" in the support directory of the source code
1040 +(which may also be installed in /usr/bin) for a way to create the tables,
1041 +populate the mounted-disk information, check files against their checksums,
1042 +and update both the MD4 and MD5 checksums for files at the same time (since
1043 +an rsync copy will only update one or the other).
1044 +
1045 +You can use a single MySQL DB for all your hosts if you give each one
1046 +their own "thishost" name and setup their device-mapping data.  Or feel
1047 +free to use separate databases, separate servers, etc.  See the rsync
1048 +daemon's "db config" parameter for how to configure a daemon to use a DB
1049 +(since a client cannot control this parameter on a daemon).
1050 +
1051  dit(bf(-a, --archive)) This is equivalent to bf(-rlptgoD). It is a quick
1052  way of saying you want recursion and want to preserve almost
1053  everything (with -H being a notable omission).
1054 diff --git a/rsyncd.conf.yo b/rsyncd.conf.yo
1055 --- a/rsyncd.conf.yo
1056 +++ b/rsyncd.conf.yo
1057 @@ -306,6 +306,18 @@ is daemon.  This setting has no effect if the "log file" setting is a
1058  non-empty string (either set in the per-modules settings, or inherited
1059  from the global settings).
1060  
1061 +dit(bf(db config)) This parameter specifies a config file to read that
1062 +holds connection details for a database of checksum information.
1063 +
1064 +The config file will be read-in prior to any chroot restrictions, but
1065 +the connection occurs from inside the chroot.  This means that you
1066 +should use a socket connection (e.g. 127.0.0.1 rather than localhost)
1067 +for a MySQL config from inside a chroot.  For SQLite, the DB file must
1068 +be placed inside the chroot (though it can be placed outside the
1069 +transfer dir if you configured an inside-chroot path).
1070 +
1071 +See the bf(--db=CONFIG_FILE) option for full details.
1072 +
1073  dit(bf(max verbosity)) This parameter allows you to control
1074  the maximum amount of verbose information that you'll allow the daemon to
1075  generate (since the information goes into the log file). The default is 1,
1076 diff --git a/support/rsyncdb b/support/rsyncdb
1077 new file mode 100755
1078 --- /dev/null
1079 +++ b/support/rsyncdb
1080 @@ -0,0 +1,331 @@
1081 +#!/usr/bin/perl -w
1082 +use strict;
1083 +
1084 +use DBI;
1085 +use Getopt::Long;
1086 +use Cwd qw(abs_path cwd);
1087 +use Digest::MD4;
1088 +use Digest::MD5;
1089 +
1090 +my $MOUNT_FILE = '/etc/mtab';
1091 +
1092 +&Getopt::Long::Configure('bundling');
1093 +&usage if !&GetOptions(
1094 +    'db=s' => \( my $db_config ),
1095 +    'init' => \( my $init_db ),
1096 +    'mounts|m' => \( my $update_mounts ),
1097 +    'recurse|r' => \( my $recurse_opt ),
1098 +    'check|c' => \( my $check_opt ),
1099 +    'verbose|v+' => \( my $verbosity = 0 ),
1100 +    'help|h' => \( my $help_opt ),
1101 +);
1102 +&usage if $help_opt || !defined $db_config;
1103 +
1104 +my %config;
1105 +open(IN, '<', $db_config) or die "Unable to open $db_config: $!\n";
1106 +while (<IN>) {
1107 +    s/[#\r\n].*//s;
1108 +    next if /^$/;
1109 +    my($key, $val) = /^(\S+):\s*(.*)/ or die "Unable to parse line $. of $db_config\n";
1110 +    $config{$key} = $val;
1111 +}
1112 +close IN;
1113 +
1114 +die "You must define at least dbtype and dbname in $db_config\n"
1115 +    unless defined $config{'dbtype'} && defined $config{'dbname'};
1116 +
1117 +my $sqlite = $config{'dbtype'} =~ /^sqlite$/i;
1118 +
1119 +my $thishost = $config{'thishost'} || 'localhost';
1120 +
1121 +my $connect = 'DBI:' . $config{'dbtype'} . ':';
1122 +$connect .= 'dbname=' . $config{'dbname'} if $sqlite;
1123 +$connect .= 'database=' . $config{'dbname'} if !$sqlite && !$init_db;
1124 +$connect .= ';host=' . $config{'dbhost'} if defined $config{'dbhost'};
1125 +$connect .= ';port=' . $config{'dbport'} if defined $config{'dbport'};
1126 +
1127 +my $dbh = DBI->connect($connect, $config{'dbuser'}, $config{'dbpass'})
1128 +    or die "DB connection failed\n";
1129 +
1130 +END {
1131 +    $dbh->disconnect if defined $dbh;
1132 +}
1133 +
1134 +if ($init_db) {
1135 +    my $unsigned = $sqlite ? '' : 'unsigned';
1136 +    my $auto_increment = $sqlite ? 'AUTOINCREMENT' : 'AUTO_INCREMENT';
1137 +    my $dbname = $config{'dbname'};
1138 +
1139 +    if (!$sqlite) {
1140 +       $dbh->do("CREATE DATABASE IF NOT EXISTS `$dbname`");
1141 +       $dbh->do("USE `$dbname`");
1142 +    }
1143 +
1144 +    print "Dropping old tables (if they exist) ...\n" if $verbosity;
1145 +    $dbh->do("DROP TABLE IF EXISTS disk") or die $dbh->errstr;
1146 +    $dbh->do("DROP TABLE IF EXISTS inode_map") or die $dbh->errstr;
1147 +
1148 +    print "Creating empty tables ...\n" if $verbosity;
1149 +    $dbh->do("
1150 +       CREATE TABLE disk (
1151 +         disk_id integer $unsigned NOT NULL PRIMARY KEY $auto_increment,
1152 +         devno bigint $unsigned NOT NULL,
1153 +         host varchar(256) NOT NULL default 'localhost',
1154 +         mounted tinyint NOT NULL default '1',
1155 +         comment varchar(256) default NULL
1156 +       )") or die $dbh->errstr;
1157 +
1158 +    $dbh->do("
1159 +       CREATE TABLE inode_map (
1160 +         disk_id integer $unsigned NOT NULL,
1161 +         ino bigint $unsigned NOT NULL,
1162 +         size bigint $unsigned NOT NULL,
1163 +         mtime bigint NOT NULL,
1164 +         ctime bigint NOT NULL,
1165 +         sum_type tinyint NOT NULL default '0',
1166 +         checksum binary(16) NOT NULL,
1167 +         PRIMARY KEY (disk_id,ino,sum_type)
1168 +       )") or die $dbh->errstr;
1169 +
1170 +    exit unless $update_mounts;
1171 +}
1172 +
1173 +my $sel_disk_H = $dbh->prepare("
1174 +    SELECT disk_id, devno, mounted, comment
1175 +    FROM disk
1176 +    WHERE host = ?
1177 +    ") or die $dbh->errstr;
1178 +
1179 +my $ins_disk_H = $dbh->prepare("
1180 +    INSERT INTO disk
1181 +    (devno, host, mounted, comment)
1182 +    VALUES(?, ?, ?, ?)
1183 +    ") or die $dbh->errstr;
1184 +
1185 +my $up_disk_H = $dbh->prepare("
1186 +    UPDATE disk
1187 +    SET mounted = ?
1188 +    WHERE disk_id = ?
1189 +    ") or die $dbh->errstr;
1190 +
1191 +my $row_id = $sqlite ? 'ROWID' : 'ID';
1192 +my $sel_lastid_H = $dbh->prepare("
1193 +    SELECT LAST_INSERT_$row_id()
1194 +    ") or die $dbh->errstr;
1195 +
1196 +my $sel_sum_H = $dbh->prepare("
1197 +    SELECT sum_type, checksum
1198 +    FROM inode_map
1199 +    WHERE disk_id = ? AND ino = ? AND size = ? AND mtime = ? AND ctime = ?
1200 +    ") or die $dbh->errstr;
1201 +
1202 +my $rep_sum_H = $dbh->prepare("
1203 +    REPLACE INTO inode_map
1204 +    (disk_id, ino, size, mtime, ctime, sum_type, checksum)
1205 +    VALUES(?, ?, ?, ?, ?, ?, ?)
1206 +    ") or die $dbh->errstr;
1207 +
1208 +my %mounts;
1209 +if ($update_mounts) {
1210 +    open(IN, $MOUNT_FILE) or die "Unable to open $MOUNT_FILE: $!\n";
1211 +    while (<IN>) {
1212 +       my($devname, $mnt) = (split)[0,1];
1213 +       next unless $devname =~ m#^/dev#;
1214 +       my($devno) = (stat($mnt))[0];
1215 +       if (!defined $devno) {
1216 +           warn "Unable to stat $mnt: $!\n";
1217 +           next;
1218 +       }
1219 +       $mounts{$devno} = "$devname on $mnt";
1220 +    }
1221 +    close IN;
1222 +}
1223 +
1224 +my %disk_id;
1225 +$sel_disk_H->execute($thishost);
1226 +while (my($disk_id, $devno, $mounted, $comment) = $sel_disk_H->fetchrow_array) {
1227 +    if ($update_mounts) {
1228 +       if (defined $mounts{$devno}) {
1229 +           if ($comment ne $mounts{$devno}) {
1230 +               if ($mounted) {
1231 +                   print "Umounting $comment ($thishost:$devno)\n" if $verbosity;
1232 +                   $up_disk_H->execute(0, $disk_id);
1233 +               }
1234 +               next;
1235 +           }
1236 +           if (!$mounted) {
1237 +               print "Mounting $comment ($thishost:$devno)\n" if $verbosity;
1238 +               $up_disk_H->execute(1, $disk_id);
1239 +           }
1240 +       } else {
1241 +           if ($mounted) {
1242 +               print "Umounting $comment ($thishost:$devno)\n" if $verbosity;
1243 +               $up_disk_H->execute(0, $disk_id);
1244 +           }
1245 +           next;
1246 +       }
1247 +    } else {
1248 +       next unless $mounted;
1249 +    }
1250 +    $disk_id{$devno} = $disk_id;
1251 +}
1252 +$sel_disk_H->finish;
1253 +
1254 +if ($update_mounts) {
1255 +    while (my($devno, $comment) = each %mounts) {
1256 +       next if $disk_id{$devno};
1257 +       print "Adding $comment ($thishost:$devno)\n" if $verbosity;
1258 +       $ins_disk_H->execute($devno, $thishost, 1, $comment);
1259 +       $sel_lastid_H->execute;
1260 +       ($disk_id{$devno}) = $sel_lastid_H->fetchrow_array;
1261 +       $sel_lastid_H->finish;
1262 +    }
1263 +    exit;
1264 +}
1265 +
1266 +my $start_dir = cwd();
1267 +
1268 +my @dirs = @ARGV;
1269 +@dirs = '.' unless @dirs;
1270 +foreach (@dirs) {
1271 +    $_ = abs_path($_);
1272 +}
1273 +
1274 +$| = 1;
1275 +
1276 +my $exit_code = 0;
1277 +
1278 +my $md4 = Digest::MD4->new;
1279 +my $md5 = Digest::MD5->new;
1280 +
1281 +while (@dirs) {
1282 +    my $dir = shift @dirs;
1283 +
1284 +    if (!chdir($dir)) {
1285 +       warn "Unable to chdir to $dir: $!\n";
1286 +       next;
1287 +    }
1288 +    if (!opendir(DP, '.')) {
1289 +       warn "Unable to opendir $dir: $!\n";
1290 +       next;
1291 +    }
1292 +
1293 +    my $reldir = $dir;
1294 +    $reldir =~ s#^$start_dir(/|$)# $1 ? '' : '.' #eo;
1295 +    print "$reldir ... \n" if $verbosity;
1296 +
1297 +    my @subdirs;
1298 +    while (defined(my $fn = readdir(DP))) {
1299 +       next if $fn =~ /^\.\.?$/ || -l $fn;
1300 +       if (-d _) {
1301 +           push(@subdirs, "$dir/$fn") unless $fn =~ /^(CVS|\.svn|\.git|\.bzr)$/;
1302 +           next;
1303 +       }
1304 +       next unless -f _;
1305 +
1306 +       my($dev,$ino,$size,$mtime,$ctime) = (stat(_))[0,1,7,9,10];
1307 +       my $disk_id = $disk_id{$dev} or next;
1308 +       $sel_sum_H->execute($disk_id,$ino,$size,$mtime,$ctime) or die $!;
1309 +       my($sum4, $dbsum4, $sum5, $dbsum5);
1310 +       my $dbsumcnt = 0;
1311 +       while (my($sum_type, $checksum) = $sel_sum_H->fetchrow_array) {
1312 +           if ($sum_type == 4) {
1313 +               $dbsum4 = $checksum;
1314 +               $dbsumcnt++;
1315 +           } elsif ($sum_type == 5) {
1316 +               $dbsum5 = $checksum;
1317 +               $dbsumcnt++;
1318 +           }
1319 +       }
1320 +       $sel_sum_H->finish;
1321 +
1322 +       next if !$check_opt && $dbsumcnt == 2;
1323 +
1324 +       if (!$check_opt || $dbsumcnt || $verbosity > 2) {
1325 +           if (!open(IN, $fn)) {
1326 +               print STDERR "Unable to read $fn: $!\n";
1327 +               next;
1328 +           }
1329 +
1330 +           while (1) {
1331 +               while (sysread(IN, $_, 64*1024)) {
1332 +                   $md4->add($_);
1333 +                   $md5->add($_);
1334 +               }
1335 +               $sum4 = $md4->digest;
1336 +               $sum5 = $md5->digest;
1337 +               print ' ', unpack('H*', $sum4), ' ', unpack('H*', $sum5) if $verbosity > 2;
1338 +               print " $fn" if $verbosity > 1;
1339 +               my($ino2,$size2,$mtime2,$ctime2) = (stat(IN))[1,7,9,10];
1340 +               last if $ino == $ino2 && $size == $size2 && $mtime == $mtime2 && $ctime == $ctime2;
1341 +               $ino = $ino2;
1342 +               $size = $size2;
1343 +               $mtime = $mtime2;
1344 +               $ctime = $ctime2;
1345 +               sysseek(IN, 0, 0);
1346 +               print " REREADING\n" if $verbosity > 1;
1347 +           }
1348 +
1349 +           close IN;
1350 +       } elsif ($verbosity > 1) {
1351 +           print "_$fn";
1352 +       }
1353 +
1354 +       if ($check_opt) {
1355 +           my $dif;
1356 +           if ($dbsumcnt == 0) {
1357 +               $dif = ' --MISSING--';
1358 +           } else {
1359 +               $dif = '';
1360 +               if (!defined $dbsum4) {
1361 +                   $dif .= ' -NO-MD4-';
1362 +               } elsif ($sum4 ne $dbsum4) {
1363 +                   $dif .= ' -MD4-CHANGED-';
1364 +               }
1365 +               if (!defined $dbsum5) {
1366 +                   $dif .= ' ---NO-MD5---';
1367 +               } elsif ($sum5 ne $dbsum5) {
1368 +                   $dif .= ' -MD5-CHANGED-';
1369 +               }
1370 +               if ($dif eq '') {
1371 +                   print " ====OK====\n" if $verbosity > 1;
1372 +                   next;
1373 +               }
1374 +               $dif =~ s/MD4-CHANGED MD5-//;
1375 +           }
1376 +           if ($verbosity < 2) {
1377 +               print $verbosity ? ' ' : "$reldir/";
1378 +               print $fn;
1379 +           }
1380 +           print $dif, "\n";
1381 +           $exit_code = 1;
1382 +       } else {
1383 +           print "\n" if $verbosity > 1;
1384 +           $rep_sum_H->execute($disk_id, $ino, $size, $mtime, $ctime, 4, $sum4);
1385 +           $rep_sum_H->execute($disk_id, $ino, $size, $mtime, $ctime, 5, $sum5);
1386 +       }
1387 +    }
1388 +
1389 +    closedir DP;
1390 +
1391 +    unshift(@dirs, sort @subdirs) if $recurse_opt;
1392 +}
1393 +
1394 +exit $exit_code;
1395 +
1396 +sub usage
1397 +{
1398 +    die <<EOT;
1399 +Usage: rsyncsums --db=CONFIG_FILE [OPTIONS] [DIRS]
1400 +
1401 +Options:
1402 +     --db=FILE     Specify the config FILE to read for the DB info.
1403 +     --init        Create (recreate) needed tables (making them empty).
1404 +                   No DIR scanning, but can be combined with --mounts.
1405 + -m, --mounts      Update mount info.  Does no DIR scanning.
1406 + -r, --recurse     Scan files in subdirectories too.
1407 + -c, --check       Check if the checksums are right (doesn't update).
1408 + -v, --verbose     Mention what we're doing.  Repeat for more info.
1409 + -h, --help        Display this help message.
1410 +EOT
1411 +}