/*
** SSYNC: Simple minded filesystem sync utility
** Copyright (C) 2002 Michael W. Shaffer <mwshaffer@angrypot.com>
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program (see the file COPYING). If not, write to:
**
** The Free Software Foundation, Inc.
** 59 Temple Place, Suite 330,
** Boston, MA 02111-1307 USA
*/
#include <unistd.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <dirent.h>
#include <utime.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "hash.h"
#include "list.h"
#include "conf.h"
#include "config_file.h"
#include "ssync.h"
#include "log.h"
#include "platform.h"
struct flags flags;
struct stats stats;
struct hash_table work;
struct hash_table dups;
enum {
data_buffer_size = 65537
};
static unsigned char * data_buffer = NULL;
/* static unsigned long min (unsigned long a, unsigned long b); */
static unsigned long max (unsigned long a, unsigned long b);
static void add_bytes (unsigned long b);
static void sync_data (char *src, struct stat *s, char *dst, struct stat *d);
static void sync_meta (char *src, struct stat *s, char *dst, struct stat *d);
static void sync_time (char *src, struct stat *s, char *dst, struct stat *d);
static void load_dir (char *dir, struct hash_table *h);
static void check_type (char *src, struct stat *s, char *dst, struct stat *d);
static void process_dir (char *src, struct stat *s, char *dst, struct stat *d);
static void process_reg (char *src, struct stat *s, char *dst, struct stat *d);
static void process_lnk (char *src, struct stat *s, char *dst, struct stat *d);
static void recursive_unlink (char *path);
void get_flags (char *buf, unsigned long bufsize)
{
if (!(buf && (bufsize > 5)))
goto EXIT;
memset (buf, 0, bufsize);
strncpy (buf, "dtmux", strlen ("dtmux"));
if (flags.no_sync_data)
buf[0] = 'D';
if (flags.no_sync_time)
buf[1] = 'T';
if (flags.no_sync_meta)
buf[2] = 'M';
if (flags.update_only)
buf[3] = 'U';
if (flags.test)
buf[4] = 'X';
EXIT:
return;
}
void get_bytes (char *buf, unsigned long bufsize)
{
if (!(buf && (bufsize > 0)))
goto EXIT;
memset (buf, 0, bufsize);
if (stats.ebytes > 0) {
snprintf (buf, (bufsize - 1),
"%ld.%ldEiB",
stats.ebytes, (stats.pbytes / 103));
} else if (stats.pbytes > 0) {
snprintf (buf, (bufsize - 1),
"%ld.%ldPiB",
stats.pbytes, (stats.tbytes / 103));
} else if (stats.tbytes > 0) {
snprintf (buf, (bufsize - 1),
"%ld.%ldTiB",
stats.tbytes, (stats.gbytes / 103));
} else if (stats.gbytes > 0) {
snprintf (buf, (bufsize - 1),
"%ld.%ldGiB",
stats.gbytes, (stats.mbytes / 103));
} else if (stats.mbytes > 0) {
snprintf (buf, (bufsize - 1),
"%ld.%ldMiB",
stats.mbytes, (stats.kbytes / 103));
} else if (stats.kbytes > 0) {
snprintf (buf, (bufsize - 1),
"%ld.%ldKiB",
stats.kbytes, (stats.bytes / 103));
} else {
snprintf (buf, (bufsize - 1),
"%ldB", stats.bytes);
}
EXIT:
return;
}
void ssync_shutdown (void) {
LOG (info, "ssync shutdown");
hash_table_free (&work);
hash_table_free (&dups);
if (data_buffer)
free (data_buffer);
return;
}
int ssync_startup (int argc, char **argv) {
char msg[256];
char sflags[6];
char *cfg = NULL;
char src_path[MAXPATHLEN + 1];
char dst_path[MAXPATHLEN + 1];
struct datum d;
memset (&work, 0, sizeof (struct hash_table));
hash_table_init (&work);
memset (&dups, 0, sizeof (struct hash_table));
dups.size = 2000;
hash_table_init (&dups);
parse_config_file (get_config_string ("work-file"), &work);
memset (src_path, 0, sizeof (src_path));
memset (dst_path, 0, sizeof (dst_path));
if ((cfg = get_config_string ("src-path")))
strncpy (src_path, cfg, (sizeof (src_path) - 1));
if ((cfg = get_config_string ("dst-path")))
strncpy (dst_path, cfg, (sizeof (dst_path) - 1));
if ((strlen (src_path) > 0) && (strlen (dst_path) > 0)) {
memset (&d, 0, sizeof (struct datum));
d.key = src_path;
d.ksize = strlen (src_path);
d.val = dst_path;
d.vsize = strlen (dst_path);
hash_table_insert (&work, &d);
}
if (!(data_buffer = malloc (data_buffer_size * sizeof (unsigned char)))) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"ssync_startup: malloc() for data_buffer of %d bytes failed: %s",
data_buffer_size, strerror (errno));
LOG (fatal, msg);
return -1;
}
memset (&flags, 0, sizeof (struct flags));
if ((cfg = get_config_string ("no-sync-data")) &&
((cfg[0] == 'y') || (cfg[0] == 'Y'))) {
flags.no_sync_data++;
}
if ((cfg = get_config_string ("no-sync-time")) &&
((cfg[0] == 'y') || (cfg[0] == 'Y'))) {
flags.no_sync_time++;
}
if ((cfg = get_config_string ("no-sync-meta")) &&
((cfg[0] == 'y') || (cfg[0] == 'Y'))) {
flags.no_sync_meta++;
}
if ((cfg = get_config_string ("update-only")) &&
((cfg[0] == 'y') || (cfg[0] == 'Y'))) {
flags.update_only++;
}
if ((cfg = get_config_string ("test")) &&
((cfg[0] == 'y') || (cfg[0] == 'Y'))) {
flags.test++;
}
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"ssync %s startup",
SSYNC_VERSION);
LOG (info, msg);
memset (sflags, 0, sizeof (sflags));
get_flags (sflags, sizeof (sflags));
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"flags: %s", sflags);
LOG (info, msg);
return 0;
}
void process (char *src, struct stat *s, char *dst, struct stat *d)
{
char msg[256];
if (!(src && s && dst && d))
goto EXIT;
if (S_ISDIR (s->st_mode)) {
check_type (src, s, dst, d);
process_dir (src, s, dst, d);
} else if (S_ISREG (s->st_mode)) {
check_type (src, s, dst, d);
process_reg (src, s, dst, d);
} else if (S_ISLNK (s->st_mode)) {
check_type (src, s, dst, d);
process_lnk (src, s, dst, d);
} else {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"skipping node '%s' of type 0x%08x",
src, (S_IFMT & s->st_mode));
LOG (warn, msg);
stats.unknowns++;
}
EXIT:
return;
}
/*
static unsigned long min (unsigned long a, unsigned long b)
{
return ((a < b) ? a : b);
}
*/
static unsigned long max (unsigned long a, unsigned long b)
{
return ((a > b) ? a : b);
}
static void add_bytes (unsigned long b)
{
if (b < 0)
goto EXIT;
stats.bytes += b;
if (stats.bytes > 1024) {
stats.kbytes += (stats.bytes / 1024);
stats.bytes %= 1024;
}
if (stats.kbytes > 1024) {
stats.mbytes += (stats.kbytes / 1024);
stats.kbytes %= 1024;
}
if (stats.mbytes > 1024) {
stats.gbytes += (stats.mbytes / 1024);
stats.mbytes %= 1024;
}
if (stats.gbytes > 1024) {
stats.tbytes += (stats.gbytes / 1024);
stats.gbytes %= 1024;
}
if (stats.tbytes > 1024) {
stats.pbytes += (stats.tbytes / 1024);
stats.tbytes %= 1024;
}
if (stats.pbytes > 1024) {
stats.ebytes += (stats.pbytes / 1024);
stats.pbytes %= 1024;
}
EXIT:
return;
}
static void sync_data (char *src, struct stat *s, char *dst, struct stat *d)
{
int sfd = -1;
int dfd = -1;
char msg[256];
unsigned long count = 0;
unsigned long bufsize = 0;
if (flags.no_sync_data || !(src && s && dst && d))
goto EXIT;
memset (data_buffer, 0, data_buffer_size);
bufsize = (max (s->st_blksize, d->st_blksize) + 1);
if (bufsize < 1025)
bufsize = 1025;
if (bufsize > data_buffer_size)
bufsize = data_buffer_size;
if ((sfd = open (src, O_RDONLY)) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"sync_data: open() on node '%s' failed: %s",
src, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
if (!flags.test) {
if ((dfd = open (dst, O_CREAT|O_WRONLY|O_TRUNC, 0600)) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"sync_data: open() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
}
memset (data_buffer, 0, bufsize);
while ((count = read (sfd, data_buffer, (bufsize - 1))) > 0) {
if (!flags.test) {
if (write (dfd, data_buffer, count) == count) {
add_bytes (count);
} else {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"sync_data: write() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
}
memset (data_buffer, 0, bufsize);
}
EXIT:
if (sfd > 0)
close (sfd);
if (dfd > 0)
close (dfd);
return;
}
static void sync_meta (char *src, struct stat *s, char *dst, struct stat *d)
{
char msg[256];
if (flags.test || flags.no_sync_meta || !(src && s && dst && d))
goto EXIT;
if (chown_nofollow (dst, s->st_uid, s->st_gid) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"sync_meta: chown_nofollow() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
}
if (S_ISDIR (d->st_mode) || S_ISREG (d->st_mode)) {
if (chmod (dst, (07777 & s->st_mode)) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"sync_meta: chmod() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
}
}
EXIT:
return;
}
static void sync_time (char *src, struct stat *s, char *dst, struct stat *d)
{
char msg[256];
struct utimbuf u;
if (flags.test || flags.no_sync_time || !(src && s && dst && d))
goto EXIT;
lstat (src, s);
lstat (dst, d);
memset (&u, 0, sizeof (struct utimbuf));
u.actime = s->st_atime;
u.modtime = s->st_mtime;
if (utime (dst, &u) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"sync_time: utime() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
}
lstat (src, s);
lstat (dst, d);
EXIT:
return;
}
static void load_dir (char *dir, struct hash_table *h)
{
DIR *dp = NULL;
struct dirent *de = NULL;
struct datum d;
struct stat s;
char path[MAXPATHLEN + 32];
char msg[256];
if (!(dir && h))
goto EXIT;
if (!(dp = opendir (dir))) {
if (!flags.test) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"load_dir: opendir() on node '%s' failed: %s",
dir, strerror (errno));
LOG (warn, msg);
stats.errors++;
}
goto EXIT;
}
memset (&d, 0, sizeof (struct datum));
d.val = &s;
d.vsize = sizeof (struct stat);
while ((de = readdir (dp))) {
if ((!de->d_name) || (!strcmp (".", de->d_name)) || (!strcmp ("..", de->d_name)))
continue;
d.key = de->d_name;
d.ksize = strlen (d.key);
memset (path, 0, sizeof (path));
snprintf (path, (sizeof (path) - 1),
"%s/%s", dir, de->d_name);
memset (&s, 0, sizeof (struct stat));
if (lstat (path, &s) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"load_dir: lstat() on node '%s' failed: %s",
path, strerror (errno));
LOG (warn, msg);
}
hash_table_insert (h, &d);
}
EXIT:
if (dp)
closedir (dp);
return;
}
static void check_type (char *src, struct stat *s, char *dst, struct stat *d)
{
int fd = -1;
char msg[256];
struct utimbuf u;
if ((flags.test || !(src && s && dst && d)) ||
((S_IFMT & s->st_mode) == (S_IFMT & d->st_mode)) ||
(flags.update_only && !(s->st_mtime > d->st_mtime))) {
goto EXIT;
}
recursive_unlink (dst);
memset (&u, 0, sizeof (struct utimbuf));
if (S_ISDIR (s->st_mode)) {
if (mkdir (dst, 0700) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"check_type: mkdir() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
if (utime (dst, &u) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"check_type: utime() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
} else if (S_ISREG (s->st_mode)) {
if ((fd = open (dst, O_CREAT|O_WRONLY|O_TRUNC, 0600)) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"check_type: open() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
close (fd);
if (utime (dst, &u) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"check_type: utime() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
} else if (S_ISLNK (s->st_mode)) {
if (symlink (".", dst) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"check_type: symlink() on node '%s' failed: %s",
dst, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
} else {
goto EXIT;
}
lstat (dst, d);
EXIT:
return;
}
static void process_dir (char *src, struct stat *s, char *dst, struct stat *d)
{
int i = 0;
struct hash_table sdir;
struct hash_table ddir;
struct list_item *curr = NULL;
struct stat t;
struct stat *st = NULL;
struct stat *dt = NULL;
struct datum *sd = NULL;
struct datum *dd = NULL;
char *sname = NULL;
char *dname = NULL;
char spath[MAXPATHLEN + 32];
char dpath[MAXPATHLEN + 32];
int f_update = 0;
int f_newerdir = 0;
char msg[256];
memset (&sdir, 0, sizeof (struct hash_table));
memset (&ddir, 0, sizeof (struct hash_table));
hash_table_init (&sdir);
hash_table_init (&ddir);
if (!(src && s && dst && d))
goto EXIT;
load_dir (src, &sdir);
load_dir (dst, &ddir);
if (s->st_mtime > d->st_mtime) {
f_newerdir++;
}
if (!flags.update_only) {
for (i = 0 ; i < ddir.size ; i++) {
for (curr = ddir.tbl[i].head ; curr ; curr = curr->next) {
if (!(dd = (struct datum *) curr->data))
continue;
if (!(sd = hash_table_search (&sdir, dd))) {
dname = (char *) dd->key;
memset (dpath, 0, sizeof (dpath));
snprintf (dpath, (sizeof (dpath) - 1),
"%s/%s", dst, dname);
recursive_unlink (dpath);
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"deleted: %s", dpath);
LOG (trace, msg);
stats.deleted++;
}
}
}
}
for (i = 0 ; i < sdir.size ; i++) {
for (curr = sdir.tbl[i].head ; curr ; curr = curr->next) {
if (!(sd = (struct datum *) curr->data))
continue;
sname = (char *) sd->key;
st = (struct stat *) sd->val;
memset (spath, 0, sizeof (spath));
snprintf (spath, (sizeof (spath) - 1),
"%s/%s", src, sname);
if ((dd = hash_table_search (&ddir, sd))) {
dname = (char *) dd->key;
dt = (struct stat *) dd->val;
memset (dpath, 0, sizeof (dpath));
snprintf (dpath, (sizeof (dpath) - 1),
"%s/%s", dst, dname);
} else {
memset (dpath, 0, sizeof (dpath));
snprintf (dpath, (sizeof (dpath) - 1),
"%s/%s", dst, sname);
memset (&t, 0, sizeof (struct stat));
dt = &t;
}
process (spath, st, dpath, dt);
}
}
if (flags.update_only) {
if (f_newerdir) {
sync_time (src, s, dst, d);
if (!((s->st_uid == d->st_uid) &&
(s->st_gid == d->st_gid) &&
(s->st_mode == d->st_mode))) {
sync_meta (src, s, dst, d);
}
f_update++;
}
} else {
if (!(s->st_mtime == d->st_mtime)) {
sync_time (src, s, dst, d);
f_update++;
}
if (!((s->st_uid == d->st_uid) &&
(s->st_gid == d->st_gid) &&
(s->st_mode == d->st_mode))) {
sync_meta (src, s, dst, d);
f_update++;
}
}
if (f_update) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"updated: %s", dst);
LOG (trace, msg);
stats.dirs_updated++;
}
stats.dirs++;
EXIT:
hash_table_free (&sdir);
hash_table_free (&ddir);
return;
}
struct ino_key {
dev_t dev;
ino_t ino;
};
static void process_reg (char *src, struct stat *s, char *dst, struct stat *d)
{
char msg[256];
char path[MAXPATHLEN + 32];
struct datum curr;
struct datum *orig = NULL;
struct ino_key ik;
int f_update = 0;
if (!(src && s && dst && d))
goto EXIT;
if (s->st_nlink > 1) {
memset (&curr, 0, sizeof (struct datum));
curr.key = &ik;
ik.dev = s->st_dev;
ik.ino = s->st_ino;
curr.ksize = sizeof (struct ino_key);
curr.val = path;
memset (path, 0, sizeof (path));
snprintf (path, (sizeof (path) - 1),
"%s", dst);
curr.vsize = strlen (curr.val);
if ((orig = hash_table_search (&dups, &curr))) {
if (orig->val && (strlen (orig->val) > 0)) {
recursive_unlink (dst);
if (link ((char *) orig->val, dst) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"process_reg: link() %s --> %s failed: %s",
dst, (char *) orig->val, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
stats.links++;
}
goto EXIT;
} else {
hash_table_insert (&dups, &curr);
}
}
if (flags.update_only) {
if (s->st_mtime > d->st_mtime) {
sync_data (src, s, dst, d);
sync_time (src, s, dst, d);
if (!((s->st_uid == d->st_uid) &&
(s->st_gid == d->st_gid) &&
(s->st_mode == d->st_mode))) {
sync_meta (src, s, dst, d);
}
f_update++;
}
} else {
if (!((s->st_mtime == d->st_mtime) &&
(s->st_size == d->st_size))) {
sync_data (src, s, dst, d);
sync_time (src, s, dst, d);
f_update++;
}
if (!((s->st_uid == d->st_uid) &&
(s->st_gid == d->st_gid) &&
(s->st_mode == d->st_mode))) {
sync_meta (src, s, dst, d);
f_update++;
}
}
if (f_update) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"updated: %s", dst);
LOG (trace, msg);
stats.regs_updated++;
}
stats.files++;
EXIT:
return;
}
static void process_lnk (char *src, struct stat *s, char *dst, struct stat *d)
{
char msg[256];
char buf0[MAXPATHLEN + 32];
char buf1[MAXPATHLEN + 32];
if (!(src && s && dst && d))
goto EXIT;
memset (buf0, 0, sizeof (buf0));
if (readlink (src, buf0, (sizeof (buf0) - 1)) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"process_lnk: readlink() on node '%s' failed: %s",
src, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
memset (buf1, 0, sizeof (buf1));
if (readlink (dst, buf1, (sizeof (buf1) - 1)) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"process_lnk: readlink() on node '%s' failed: %s",
src, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
stats.links++;
if (!strcmp (buf0, buf1))
goto EXIT;
if (!flags.test) {
recursive_unlink (dst);
if (symlink (buf0, dst) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"process_lnk: symlink() %s --> %s failed: %s",
dst, buf0, strerror (errno));
LOG (warn, msg);
stats.errors++;
goto EXIT;
}
}
sync_meta (src, s, dst, d);
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"updated: %s", dst);
LOG (trace, msg);
stats.lnks_updated++;
EXIT:
return;
}
static void recursive_unlink (char *path)
{
int i = 0;
struct stat s;
struct hash_table dir;
struct datum *d = NULL;
struct list_item *curr = NULL;
char cpath[MAXPATHLEN + 32];
char msg[256];
memset (&dir, 0, sizeof (struct hash_table));
hash_table_init (&dir);
if (flags.test || !(path && (strlen (path) > 0)))
goto EXIT;
memset (&s, 0, sizeof (struct stat));
if (lstat (path, &s) < 0)
goto EXIT;
if (S_ISDIR (s.st_mode)) {
load_dir (path, &dir);
for (i = 0 ; i < dir.size ; i++) {
for (curr = dir.tbl[i].head ; curr ; curr = curr->next) {
if (!(d = (struct datum *) curr->data))
continue;
memset (cpath, 0, sizeof (cpath));
snprintf (cpath, (sizeof (cpath) - 1),
"%s/%s", path, (char *) d->key);
recursive_unlink (cpath);
}
}
if (rmdir (path) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"recursive_unlink: rmdir() on node '%s' failed: %s",
path, strerror (errno));
LOG (warn, msg);
stats.errors++;
}
} else {
if (unlink (path) < 0) {
memset (msg, 0, sizeof (msg));
snprintf (msg, (sizeof (msg) - 1),
"recursive_unlink: unlink() on node '%s' failed: %s",
path, strerror (errno));
LOG (warn, msg);
stats.errors++;
}
}
EXIT:
hash_table_free (&dir);
return;
}
syntax highlighted by Code2HTML, v. 0.9.1