#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* needed for S_IFMT and AT_FDCWD */ #include #include #include "sqlite3.h" #include "zpm.h" #include "lib/buffer.h" struct config { struct zpm *log; /* logging db will be attached as "log" */ struct zpm *src; char *dbfile; char *rootdir; int errabort, errors, verbose, dryrun, conflicts; int setuser, setgroup; int reverse, exitonerror; int overwrite, accept, acceptdir, ignoredirmd; unsigned long ops_total, ops_completed; unsigned long ops_remove, ops_remove_completed; unsigned long ops_update, ops_update_completed; unsigned long ops_install, ops_install_completed; int progress; /* type of progress meter */ struct zpm_buffer note; }; struct nitem { int op; char *opstr; uid_t uid; gid_t gid; char *dest; char *path; char *hash, *ohash; char *mds, *omds; char *target; char *pkglist; /* space separated */ time_t mtime; mode_t mode; int ftype; int configuration, oldwasconf; struct timespec times[2]; }; static void usage() { printf("usage: zpm syncfs ...\n"); } static void warn(char *fmt, ...) { va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); fprintf(stderr, "\n"); } static void add_to_note(struct config *cf, char *fmt, ...) { va_list ap; va_start(ap, fmt); zpm_buffer_appendvf(&cf->note, fmt, ap); va_end(ap); } static void pdots(int len, int ch, int was, int now, int total) { was = len * was / total; if (now > total) { now = total; } now = len * now / total; while (was++ < now) { putchar(ch); } fflush(stdout); } static int seterror(struct config *conf, char *msgfmt, ...) { char msg[1024]; va_list ap; conf->errors++; va_start(ap, msgfmt); vsnprintf(msg, sizeof msg, msgfmt, ap); va_end(ap); msg[1023] = 0; if (conf->log->errmsg) { free(conf->log->errmsg); } conf->log->errmsg = strdup(msg); if (conf->verbose) { fprintf(stderr, "%s\n", msg); } return conf->errabort; } static int setsyserr(struct config *conf, char *msgfmt, ...) { char msg[1024]; va_list ap; int printed; conf->errors++; va_start(ap, msgfmt); printed = vsnprintf(msg, sizeof msg, msgfmt, ap); va_end(ap); if (printed < 1) { /* nothing we can really do */ return conf->errabort; } if ((size_t)printed < sizeof msg) { snprintf(msg+printed, sizeof msg - printed, ": %s", strerror(errno)); } msg[1023] = 0; if (conf->log->errmsg) { free(conf->log->errmsg); } conf->log->errmsg = strdup(msg); if (conf->verbose) { fprintf(stderr, "%s\n", msg); } return conf->errabort; } static int exists(char *path, mode_t *mode) { struct stat st; if (lstat(path, &st) == -1) { return 0; } if (mode) *mode = st.st_mode; return 1; } /* TODO maintain a list of already created directories */ static int create_leading_dirs(char *path) { char *delim, *s; int ch = 0; char pcopy[ZPM_PATH_MAX]; struct stat st; strcpy(pcopy, path); delim = strrchr(pcopy, '/'); if (!delim) return 1; /* not an error, but no leading dirs */ /* cut off last component */ *delim = 0; s = pcopy; do { while (*s == '/') { s++; } delim = strchr(s, '/'); if (delim) { ch = *delim; *delim = 0; } /* try to create the directory, if it exists * and is a directory or a symlink, that's ok * should be (eventually) a symlink to a directory * so we want stat here, not lstat */ if (mkdir(pcopy, 0755) == -1) { switch (errno) { case EEXIST: if (stat(pcopy, &st) == -1) { /* can't stat? */ return 0; } switch (st.st_mode & S_IFMT) { case S_IFDIR: break; default: return 0; } break; default: return 0; } } if (delim) { *delim = ch; } s = delim; } while (delim); return 1; } static char *column(char *col, int ncols, char **vals, char **cols) { int i = 0; char *val = NULL; for (i=0; i < ncols; i++) { // fprintf(stderr, "checking '%s' = '%s'\n", cols[i], vals[i]); if (!strcmp(col, cols[i])) { val = vals[i]; break; } } return val; } #define COL(x) column(x, ncols, vals, cols) #define SYSERR(x) do { conf->log->error = 2; return conf->errabort; } while (0) /* TODO handle other ops properly */ static char *ops[] = { "new", "remove", "update", 0 }; enum op { OP_NEW = 1, OP_REMOVE = 2, OP_UPDATE = 3 }; static int getop(char *opstr) { int i; if (!opstr) return 0; for (i=0;ops[i];i++) { if (!strcmp(opstr, ops[i])) { return i+1; } } return 0; } static int report_conflicts(void *f, int ncols, char **vals, char **cols) { struct config *conf = f; char *path, *hash, *pkg, *conflict_type, *mds; pkg = COL("pkgid"); path = COL("path"); conflict_type = COL("conflict"); if (!strcmp(conflict_type, "hash")) { hash = COL("hash"); fprintf(stderr, "hash conflict: package %s path %s hash %.8s\n", pkg, path, hash); } else if (!strcmp(conflict_type, "md")) { mds = COL("mds"); fprintf(stderr, "md conflict: package %s path %s md %s\n", pkg, path, mds); } else { fprintf(stderr, "%s conflict: package %s path %s\n", conflict_type, pkg, path); } conf->conflicts++; return 0; } static int read_item(struct config *conf, int ncols, char **vals, char **cols, struct nitem *n) { char *val; long lval; struct passwd *pw; struct group *gr; struct nitem zero = { 0 }; *n = zero; val = COL("op"); if (!val) { seterror(conf, "can't determine op"); return 0; } n->opstr = val; n->op = getop(val); if (!n->op) { seterror(conf, "can't determine op"); return 0; } n->path = COL("path"); if (!n->path) { seterror(conf, "no file path"); return 0; } if (strlen(n->path) == 0) { seterror(conf, "zero length path not allowed"); return 0; } /* TODO config to dishonor setuid/setgid */ n->dest = COL("dest"); if (!n->dest) { seterror(conf, "no file dest"); return 0; } if (strlen(n->dest) == 0) { seterror(conf, "zero length dest not allowed"); return 0; } val = COL("mode"); if (!val) { seterror(conf, "can't determine mode"); return 0; } n->mode = strtoul(val, NULL, 8); val = COL("configuration"); if (!val) { seterror(conf, "can't determine config status"); return 0; } lval = strtol(val, NULL, 10); n->configuration = ((lval & 1) != 0); n->oldwasconf = ((lval & 2) != 0); val = COL("filetype"); if (!val || strlen(val) == 0) { seterror(conf, "can't determine file type"); return 0; } n->ftype = *val; /* these can be null */ n->ohash = COL("ohash"); n->mds = COL("mds"); n->omds = COL("omds"); n->pkglist = COL("pkgid"); if (n->ftype == 'r') { n->hash = COL("hash"); if (!n->hash) { seterror(conf, "can't get hash"); return 0; } } else if (n->ftype == 'l') { n->target = COL("target"); if (!n->target) { seterror(conf, "can't get target"); return 0; } if (strlen(n->target) == 0) { seterror(conf, "zero length target not allowed"); return 0; } n->hash = n->target; } if (conf->setuser) { val = COL("username"); if (!val) { seterror(conf, "no username"); return 0; } pw = getpwnam(val); if (!pw) { seterror(conf, "no passwd entry"); return 0; } n->uid = pw->pw_uid; } else { n->uid = geteuid(); } if (conf->setgroup) { val = COL("groupname"); if (!val) { seterror(conf, "no groupname"); return 0; } gr = getgrnam(val); if (!gr) { seterror(conf, "no group entry for %s", val); return 0; } n->gid = gr->gr_gid; } else { n->gid = getegid(); } errno = 0; double mtime = strtod(COL("mtime"),NULL); if (errno) { mtime = (double)time(NULL); } n->mtime = (time_t)mtime; n->times[0].tv_sec = 0; n->times[0].tv_nsec = UTIME_OMIT; n->times[1].tv_sec = (time_t)llrint(floor(mtime)); n->times[1].tv_nsec = lrint(floor(fmod(mtime,1.0)*1000000000)); return 1; } /* file does not exist */ #define D_NOEXIST 0x1 /* files are different types */ #define D_TYPE 0x2 /* metadata is different */ #define D_MD 0x4 /* content or link target is different */ #define D_HASH 0x8 /* file to be installed is a directory */ #define D_ISDIR 0x10 /* path on disk is a directory */ #define D_EISDIR 0x20 /* usernames different */ #define D_UID 0x40 /* group names different */ #define D_GID 0x80 /* file mode is different */ #define D_MODE 0x100 /* mtimes are different */ #define D_MTIME 0x200 /* the hash of the file we are supposedly replacing is different than * the the hash of the file on disk */ #define D_OHASH 0x400 /* file exists, and is a directory, and is empty */ #define D_ISEMPTY 0x800 /* an error occurred trying to compare the file (other than it doesn't exist */ #define D_ERROR 0x1000 /* there was a stat error */ #define D_STATERROR 0x2000 /* there was an error calling readlink */ #define D_RLERROR 0x4000 static int dir_is_empty(char *path) { DIR *dir; struct dirent *dp; int empty; dir = opendir(path); if (!dir) { return -1; } dp = readdir(dir); if (dp) { empty = 0; } else { empty = 1; } closedir(dir); return empty; } /* 1 = file doesn't exist, 2 = file is a directory, target isn't */ /* 4 == ftype different */ /* 8 = hash different when both are regular files */ static unsigned int file_compare(struct nitem *n, struct stat *st) { int etype = 0, stat_type; char ehash[ZPM_HASH_STRLEN+1]; unsigned int diff = 0; char link[1024]; ssize_t lsize; switch (n->ftype) { case 'd': etype = S_IFDIR; diff |= D_ISDIR ; break; case 'r': etype = S_IFREG; break; case 'l': etype = S_IFLNK; break; default: etype = 0; break; } errno = 0; /* new file, so check type, hash, etc */ if (lstat(n->dest, st) == 0) { stat_type = st->st_mode & S_IFMT; if (stat_type != etype) { diff |= D_TYPE; } if (stat_type == S_IFDIR) { diff |= D_EISDIR; if (dir_is_empty(n->dest)) { diff |= D_ISEMPTY; } } if (n->hash && etype == S_IFREG && stat_type == S_IFREG) { zpm_hash(n->dest, ehash, 0); if (strcmp(n->hash, ehash) != 0) { diff |= D_HASH; } if (n->ohash && strcmp(n->ohash, ehash) != 0) { diff |= D_OHASH; } } if (n->hash && etype == S_IFLNK && stat_type == S_IFLNK) { lsize = readlink(n->dest, link, sizeof link); if (lsize == -1 || lsize == sizeof link) { diff |= D_RLERROR; diff |= D_ERROR; } else { link[lsize] = 0; if (strcmp(n->target, link) != 0) { diff |= D_HASH; } } } if (n->uid != st->st_uid) { diff |= D_UID; diff |= D_MD; } if (n->gid != st->st_gid) { diff |= D_GID; diff |= D_MD; } if (n->mode != (st->st_mode & 07777)) { diff |= D_MODE; diff |= D_MD; } } else { switch(errno) { case ENOENT: diff |= D_NOEXIST; break; default: diff |= (D_STATERROR|D_ERROR); break; } } return diff; } /* 0 = not acceptable * 1 = accept and create/update/remove * 2 = accept as is * 3 = remove and overwrite * 4 = update metadata */ static int acceptable(struct config *conf, unsigned int diffs, int op) { int exist = (!(diffs & D_NOEXIST)); int sametype = (!(diffs & D_TYPE)); int mdsame = (!(diffs & D_MD)); int hashsame = (!(diffs & D_HASH)); int isdir = (diffs & D_ISDIR); if (!exist) { return op == OP_REMOVE ? 2 : 1; } if (op == OP_UPDATE) { return sametype ? 4 : 3; } if (op == OP_REMOVE) { return 1; } /* the hard cases, should be installing new, but already exists */ if (!sametype) { return conf->overwrite ? 3 : 0; } if (mdsame && (conf->accept || conf->overwrite)) { return 1; } if (isdir) { if (mdsame || conf->ignoredirmd) { return conf->acceptdir ? 2 : 0; } if (conf->overwrite) { return 4; } } if (hashsame && (conf->accept || conf->overwrite)) { return 1; } return conf->overwrite ? 3 : 0; } static int check_existing(void *f, int ncols, char **vals, char **cols) { struct config *conf = f; struct stat st; struct nitem nitem; if (!read_item(conf, ncols, vals, cols, &nitem)) { fprintf(stderr, "can't read item\n"); return conf->errabort; } if (conf->verbose > 1) { fprintf(stderr, "check for existing %s\n", nitem.path); } if (lstat(nitem.path, &st) == -1) { switch(errno) { /* not an error, file shouldn't exist*/ case ENOENT: break; default: fprintf(stderr, "unable to check %s: %s\n", nitem.path, strerror(errno)); conf->errors++; break; } return 0; } unsigned int diffs = file_compare(&nitem, &st); int sametype = (!(diffs & D_TYPE)); if (diffs >= D_ERROR) { return seterror(conf, "can't check %s", nitem.dest); } if (sametype && nitem.configuration) { return 0; } int action = acceptable(conf, diffs, nitem.op); if (!action) { if (conf->accept) { fprintf(stderr, "%s exists and is not acceptable\n", nitem.path); } else { fprintf(stderr, "%s exists\n", nitem.path); } conf->errors++; } return 0; } static void update_progress(struct config *conf, char *op, char *path) { if (!conf->verbose) { return; } if (conf->progress == 0) { pdots(50, '.', conf->ops_completed-1, conf->ops_completed, conf->ops_total); } else if (conf->progress == 1) { size_t len = strlen(path); int offset = 0; if (len > 50) { offset = len-50; } printf("\r%lu/%lu %.10s %.50s\n", conf->ops_completed, conf->ops_total, op, path+offset); } else if (conf->progress == 2) { printf("%lu/%lu %s %s\n", conf->ops_completed, conf->ops_total, op, path); } fflush(stdout); } static int remove_files(void *f, int ncols, char **vals, char **cols) { struct config *conf = f; char *dest; struct stat st; int flags = 0; conf->ops_completed++; dest = COL("dest"); char *ftype = COL("filetype"); if (!dest) return seterror(conf,"no file dest"); if (!ftype) return seterror(conf,"no file type"); update_progress(conf, *ftype == 'd' ? "rmdir" : "unlink", dest); if (conf->dryrun) { return 0; } errno = 0; if (lstat(dest, &st) == -1) { switch (errno) { case ENOENT: if (conf->verbose > 1) { fprintf(stderr, "expected file not found: '%s'\n", dest); } break; default: return seterror(conf, "can't stat %s: %s", dest, strerror(errno)); } return 0; } if (S_ISDIR(st.st_mode)) { flags = AT_REMOVEDIR; } /* TODO check that expected filetype matches actual filetype */ /* alternatively, just use the expected type, skip the stat, * and let it fail if the type is wrong */ errno = 0; if (unlinkat(AT_FDCWD, dest, flags) == -1) { switch (errno) { case ENOENT: break; case ENOTEMPTY: /* fall through */ case EEXIST: fprintf(stderr, "expected empty directory: %s\n", dest); break; default: return seterror(conf, "can't unlink %s: %s", dest, strerror(errno)); } } return 0; } #define MARK fprintf(stderr, "%s %d: mark\n", __func__, __LINE__) static int remove_dir(struct config *conf, char *path) { int rv; rv = rmdir(path); if (rv == -1) { setsyserr(conf, "can't rmdir %s", path); return 0; } return 1; } static int remove_existing(struct config *conf, char *path) { int rv; rv = unlink(path); if (rv == -1) { setsyserr(conf, "can't unlink %s", path); return 0; } return 1; } static int set_md(struct config *conf, struct nitem *item) { int rv; int success = 0; if (conf->dryrun) { if (item->ftype != 'l') { printf("chmod %o %s\n", item->mode, item->dest); } if (conf->setuser && conf->setgroup) { printf("lchown %d:%d %s\n", item->uid, item->gid, item->dest); } printf("mtime %.0f %s\n", (double)item->mtime, item->dest); fflush(stdout); return success; } if (conf->setuser && conf->setgroup) { rv = lchown(item->dest, item->uid, item->gid); if (rv == -1) { setsyserr(conf, "can't lchown %s", item->dest); return conf->errabort; } } /* have to chmod after the chown, setuid bits may (and will) * be cleared after a chown */ /* can't chmod a symlink */ if (item->ftype != 'l') { rv = chmod(item->dest, item->mode); if (rv == -1) { setsyserr(conf, "can't chmod %o %s", item->mode, item->dest); return conf->errabort; } } rv = utimensat(AT_FDCWD, item->dest, item->times, AT_SYMLINK_NOFOLLOW); if (rv == -1) { setsyserr(conf, "can't set mtime %.0f %s", (double)item->mtime, item->dest); return conf->errabort; } return 0; } /* install a file or create a directory or symlink. path should not exist * at this point. */ /* flags: 1 = set md, 2 = create leading dirs, 4 = unlink existing file, * 8 = rmdir existing dir, 16 = return true/false */ #define INS_MD 0x1 #define INS_CLD 0x2 #define INS_UNLINK 0x4 #define INS_RMDIR 0x8 #define INS_RTF 0x10 #define INS_ZPMNEW 0x20 static int install(struct config *conf, struct nitem *item, unsigned int flags) { int rv = 1; struct zpm *source; int mkleading = (flags & 2); int setmd = (flags & 1); int unlink_file = (flags & 4); int rm_dir = (flags & 8); int failure = conf->errabort; int success = 0; if (flags & INS_RTF) { failure = 0; success = 1; } if (conf->dryrun) { if (unlink_file) { printf("unlink %s\n", item->dest); } else if (rm_dir) { printf("rmdir %s\n", item->dest); } printf("install %c%o %d:%d %s", item->ftype, item->mode, item->uid, item->gid, item->dest); if (item->ftype == 'l') { printf(" -> %s", item->target); } printf("\n"); fflush(stdout); return success; } source = conf->src ? conf->src : conf->log; if (mkleading) { rv = create_leading_dirs(item->dest); if (!rv) { setsyserr(conf, "can't create leading dirs for %s", item->dest); return failure; } } if (unlink_file) { rv = remove_existing(conf, item->dest); } else if (rm_dir) { rv = remove_dir(conf, item->dest); } if (rv != 1) { return failure; } errno = 0; switch (item->ftype) { case 'r': rv = zpm_extract(source, item->hash, item->dest, item->mode); if (rv == 0) rv = -1; break; case 'd': rv = mkdir(item->dest, item->mode); break; case 'l': rv = symlink(item->target, item->dest); break; default: /* error */ break; } if (rv == -1) { switch (item->ftype) { case 'r': seterror(conf, "can't extract %s", item->dest); break; case 'd': setsyserr(conf, "install mkdir(\"%s\") failed", item->dest); break; case 'l': setsyserr(conf, "install symlink(\"%s\") failed", item->dest); break; } setsyserr(conf, "installing %s failed", item->dest); return failure; } if (setmd) { return set_md(conf, item) == 0 ? success : failure; } return success; } static int save_config_file(struct config *conf, struct nitem *n, char *msgfmt) { char hash[ZPM_HASH_STRLEN+1]; if (!msgfmt) { msgfmt = "saved config file %.8s"; } if (zpm_import(conf->log, n->dest, 0, hash)) { zpm_note_add(conf->log, n->pkglist, n->path, hash, msgfmt, hash); } else { warn("unable to import existing config file %s", n->dest); conf->errors++; return 0; } return 1; } #if 0 /* * figure out what the difference is for a config file, only called * for an update of a configuration file * return -1 on an error * return 1 if the new file should not be installed * return 0 if the new file should be installed */ static int adjust_for_config(struct nitem *n, unsigned int diffs) { if (!n->oldwasconf) { return 0; } int sametype = (!(diffs & D_TYPE)); int isdir = (diffs & D_ISDIR); int eisdir = (diffs & D_EISDIR); /* what if old was a directory? */ if (!n->configuration) { /* replacing conf with non-conf */ /* absorb file, mark todo */ return 0; } /* both old and new are config files */ if (isdir && sametype) { /* both config directories, can only be changing * metadata, so no adjustment needed */ return 0; } if (isdir) { return 0; } if (eisdir) { /* replacing old conf directory with a conf file. * nothing needs to be done, if the directory * is empty, it's ok to remove. if it's not empty, * the install will fail */ return 0; } /* replacing old file with new file */ /* new is same as on disk */ if (!(diffs & D_HASH)) { return 0; } /* new is different than on disk, but on disk is same as old */ if (!(diffs & D_OHASH)) { return 0; } return 1; } #endif static int config_handler(void *f, int ncols, char **vals, char **cols) { struct config *conf = f; struct nitem nitem; struct stat existing; char *save = 0; char *note = 0; char *notehash = 0; int update = 0; if (!read_item(conf, ncols, vals, cols, &nitem)) { fprintf(stderr, "can't read item\n"); conf->errors++; return conf->errabort; } unsigned int diffs = file_compare(&nitem, &existing); if (diffs >= D_ERROR) { return seterror(conf, "can't check %s", nitem.dest); } int exist = (!(diffs & D_NOEXIST)); int sametype = (!(diffs & D_TYPE)); //int mdsame = (!(diffs & D_MD)); int hashsame = (!(diffs & D_HASH)); int oldhashsame = (!(diffs & D_OHASH)); int isdir = (diffs & D_ISDIR); int eisdir = (diffs & D_EISDIR); update = (nitem.op == OP_UPDATE); notehash = nitem.hash; /* if the file doesn't exist in the system, nothing to do */ /* could possibly note if we expected it, but the regular handling * should do that */ if (!exist) { return 0; } if (nitem.op == OP_UPDATE && !nitem.oldwasconf) { /* possibly save anyway */ return 0; } /* so, old was conf, and something exists in the filesystem */ if (!sametype) { warn("won't %s %s%s, %s exists", nitem.op == OP_NEW ? "install" : nitem.opstr, nitem.path, isdir ? "/" : "", eisdir ? "directory" : "file" ); conf->errors++; return conf->errabort; } /* all below are same type of file */ /* what about sametype, but old was different type */ if (isdir) { return 0; } /* save or note cases */ if (nitem.op == OP_REMOVE) { save ="saved removed config file %.8s"; } else if (nitem.op == OP_UPDATE) { if (!nitem.configuration) { /* replacing config with non-config */ save = "replacing configuration file %.8s with non-configuration file"; } else if (oldhashsame) { /* config file hasn't changed from old default, * so go ahead and install the new one */ save = "replaced old default config (%.8s) with new one."; notehash = nitem.ohash; } else { note = "kept existing config file. new default version is %.8s"; save = 0; } } else if (nitem.op == OP_NEW && !hashsame) { note = "config file already existed. would have installed %.8s"; save = 0; } /* * save files, add notes */ if (!conf->dryrun) { if (save) { warn("saving config file: %s (root %s)", nitem.path, conf->rootdir ? conf->rootdir : "/"); save_config_file(conf, &nitem, save); } if (note) { fprintf(stderr, "%s (%s) '%s' ", nitem.pkglist, nitem.hash, nitem.path); fprintf(stderr, note, nitem.hash); fprintf(stderr, "\n"); zpm_note_add(conf->log, nitem.pkglist, nitem.path, nitem.hash, note, nitem.hash); } } else { if (save) { fprintf(stderr, "dry run: %s %s: ", nitem.pkglist, nitem.path); warn(save, notehash); } if (note) { fprintf(stderr, "dry run: %s %s: ", nitem.pkglist, nitem.path); warn(note, notehash); } } return 0; } static void handle_config_files(struct config *conf) { int rv; char *errmsg; sqlite3_str *s; char *sql; s = sqlite3_str_new(conf->log->db); sqlite3_str_appendall(s, "select *, "); if (conf->rootdir) { sqlite3_str_appendf(s, "printf('%%s/%%s',rtrim(%Q,'/'),ltrim(path,'/'))", conf->rootdir); } else { sqlite3_str_appendf(s, "printf('/%%s', trim(path, '/'))"); } sqlite3_str_appendall(s, " as dest from syncinfo"); sqlite3_str_appendall(s," where configuration > 0 and op in ('new','update','remove')"); if (conf->reverse) { sqlite3_str_appendall(s," order by length(path) desc, path desc"); } sql = sqlite3_str_value(s); rv = zpm_exec(conf->log, sql, config_handler, conf, &errmsg); sqlite3_str_finish(s); if (rv) { fprintf(stderr, "exec fail: %s\n", sqlite3_errstr(rv)); if (errmsg) { fprintf(stderr, "database error: %s\n", errmsg); conf->errors++; } if (conf->log->error == 1) { fprintf(stderr, "unable to allocate memory\n"); } fprintf(stderr, "zpm_exec failure: %s\n", conf->log->errmsg ? conf->log->errmsg : "unknown"); conf->errors++; } if (conf->errors && conf->exitonerror) { zpm_close(conf->log); zpm_close(conf->src); exit(EXIT_FAILURE); } } static int install_files(void *f, int ncols, char **vals, char **cols) { struct config *conf = f; struct nitem nitem; struct stat existing; int update = 0; /* put the result row in a hash table? May not actually * be faster */ if (!read_item(conf, ncols, vals, cols, &nitem)) { fprintf(stderr, "can't read item\n"); return conf->errabort; } #if 0 int64_t used, high; used = sqlite3_memory_used()/1024/1024; high = sqlite3_memory_highwater(0)/1024/1024; fprintf(stderr, "memory = %ld MB / %ld MB\n", used, high); #endif char action[40]; sprintf(action, "%.8s %c", nitem.opstr, nitem.ftype); conf->ops_completed++; update_progress(conf, action, nitem.dest); unsigned int diffs = file_compare(&nitem, &existing); if (diffs >= D_ERROR) { return seterror(conf, "can't check %s", nitem.dest); } /* updates: * exist & same type & md same & hash same: do nothing, but warn bug * exist & same type & md diff & hash same: fix md * exist & same type & md same & hash diff: replace * exist & same type & md diff & hash diff: replace & fix * no exist: install and warn * dir & not dir : remove, mkdir * not dir & not dir & diff type: remove, install * not dir & dir : remove dir if empty, error if not empty, install * * installs: * no exist: create leading dirs, install * * exist & same type & md same & hash same & accept or over: do nothing * exist & same & md diff or hash diff & overwrite : update * exist & same & md diff or hash diff & accept : error, can't accept * exist & same & md diff or hash diff & not accept : error * * exist & different type & not overwrite : error * not dir & not dir & overwrite : remove and install * not dir & dir & overwrite: remove empty or error, install * dir & dir & overwrite: fix md * dir & not dir & overwrite: remove and mkdir */ int exist = (!(diffs & D_NOEXIST)); int sametype = (!(diffs & D_TYPE)); int mdsame = (!(diffs & D_MD)); int hashsame = (!(diffs & D_HASH)); int ohashsame = (!(diffs & D_OHASH)); int isdir = (diffs & D_ISDIR); int eisdir = (diffs & D_EISDIR); int accept = conf->accept; int overwrite = conf->overwrite; int installing = (nitem.op == OP_NEW); update = (nitem.op == OP_UPDATE); /* if a config file doesn't exist on disk, go ahead and do * whatever you were going to do, this logic here just * needs to determine if we should skip what we were going to do * * if the old item was a configuration item and the new one isn't, it * will have been saved earlier, so we can just go ahead. so we only * test for an existing file, where the item is a configuration file */ if (nitem.configuration && exist) { if (!sametype && !conf->overwrite) { return seterror(conf, "configuration file exists with different type: %s", nitem.dest); } if (conf->accept) { return 0; } if (isdir && !mdsame) return 0; if (!isdir && !ohashsame) return 0; } if (update) { if (!exist) { /* warn, it should exist */ add_to_note(conf, "%s missing, installing\n", nitem.dest); fprintf(stderr, "%s missing, installing\n", nitem.dest); return install(conf, &nitem, 3); } /* file exists in filesystem */ if (sametype) { if (mdsame && hashsame) { /* warn, bug in logic. This shouldn't occur, * because if there is nothing to do, it * shouldn't be listed as an update */ /* could be an update. We're checking against * what's actually on disk, not what was * expected to have been on disk. So, if * the admin has modified the file, or if * it had been installed ignoring the user * and group, it might be correct on disk * but not as in the local database */ /* TODO detect whether this a logic bug or * an on-disk difference */ #if 0 fprintf(stderr, "%s should not be an update\n", nitem.dest); fprintf(stderr, "old hash: %s\n", nitem.ohash); fprintf(stderr, "new hash: %s\n", nitem.hash); fprintf(stderr, "old mds: %s\n", nitem.omds); fprintf(stderr, "new mds: %s\n", nitem.mds); #endif /* do nothing */ return 0; } if (!mdsame && hashsame) { /* fix md */ return set_md(conf, &nitem); } if (!hashsame) { /* doesn't matter on the md */ int flags = INS_MD | INS_CLD; if (nitem.ftype == 'l') { flags |= INS_UNLINK; } return install(conf, &nitem, flags); } } /* file exists, and is not the same type */ if (isdir && !eisdir) { /* remove existing */ /* mkdir */ return install(conf, &nitem, 7); } if (!isdir && eisdir) { /* remove dir, or error */ /* install */ return install(conf, &nitem, 11); } if (!isdir && !isdir) { /* necessarily !sametype, sametype handled above */ /* remove existing */ /* install */ return install(conf, &nitem, 7); } /* error, should not be possible, assert(0)? */ fprintf(stderr,"impossible state: %s:%d\n", __func__, __LINE__); } if (installing) { if (!exist) { return install(conf, &nitem, 3); } /* file exists in filesystem */ if (sametype) { if (mdsame && hashsame && (accept || overwrite)) { /* do nothing */ if (conf->dryrun || conf->verbose > 1) { fprintf(stderr, "accept %s: %s\n", eisdir ? "directory" : "file", nitem.dest); } return 0; } if (mdsame && isdir && conf->acceptdir) { return 0; } if (!mdsame && isdir && conf->ignoredirmd) { if (conf->verbose > 1) { fprintf(stderr, "ignoring directory metadata difference: %s\n", nitem.dest); } return 0; } if (mdsame && hashsame && !(accept || overwrite)) { /* error */ return seterror(conf, "file exists: %s", nitem.dest); } if (mdsame && !hashsame && overwrite) { /* install */ return install(conf, &nitem, eisdir ? 11 : 7); } if (nitem.configuration && accept) { /* accept a changed config file */ if (conf->dryrun || conf->verbose) { fprintf(stderr, "accept %smodified config %s: %s\n", (!mdsame || !hashsame) ? "" : "un", eisdir ? "directory" : "file", nitem.dest); } return 0; } if (mdsame && !hashsame && !overwrite) { /* accept doesn't matter, since it's * not an acceptable file */ /* error */ if (nitem.ftype == 'l') { char link[1024]; ssize_t lsize; lsize = readlink(nitem.dest, link, sizeof link); if (lsize == -1 || (size_t)lsize >= sizeof link) { return seterror(conf, "%s (linkdiff): expecting %s -> %s, unable to read link", accept ? "existing file not acceptable" : "file exists", nitem.dest, nitem.target, link); } else { link[lsize] = 0; /* links must be different */ return seterror(conf, "%s (linkdiff): expecting %s -> %s, have -> %s", accept ? "existing file not acceptable" : "file exists", nitem.dest, nitem.target, link); } } else { return seterror(conf, "%s (hashdiff): %s", accept ? "existing file not acceptable" : "file exists", nitem.dest); } } if (!mdsame && hashsame && overwrite) { /* fix md */ return set_md(conf, &nitem); } if (!mdsame && hashsame && !overwrite) { /* accept doesn't matter, since it's * not an acceptable file */ /* error */ return seterror(conf, "%s (mddiff): %s", accept ? "existing file not acceptable" : "file exists", nitem.dest); } if (!mdsame && !hashsame && overwrite) { /* install */ return install(conf, &nitem, eisdir ? 11 : 7); } if (!mdsame && !hashsame && !overwrite) { /* accept doesn't matter, since it's * not an acceptable file */ /* error */ return seterror(conf, "%s (md+hash): %s", accept ? "existing file not acceptable" : "file exists", nitem.dest); } /* error, should be impossible */ return seterror(conf, "impossible state reached"); } /* file exists, and is not the same type */ if (!overwrite) { /* error */ return seterror(conf, "%s (difftype): %s", accept ? "existing file not acceptable" : "file exists", nitem.dest); } /* not the same type, but ok to overwrite */ if (!eisdir) { /* remove existing */ return install(conf, &nitem, 7); } /* existing path is a directory */ if (isdir) { /* fix md */ /* impossible, if isdir and eisdir, would be same type */ return set_md(conf, &nitem); } else { /* remove empty dir or error */ /* install */ return install(conf, &nitem, 11); } /* if we get here, we missed a case */ return seterror(conf, "impossible state 2 reached"); } /* TODO extra verbose print perms, mtime, etc, probably ls -l format */ if (conf->verbose) { printf("%s\n", nitem.path); } return 0; } static void check_conflicts(struct config *conf, char *conflict_type, int (callback)(void *, int, char **, char **)) { int rv; char *errmsg; sqlite3_str *s; char *sql; s = sqlite3_str_new(conf->log->db); sqlite3_str_appendall(s, "select *, "); if (conf->rootdir) { sqlite3_str_appendf(s, "printf('%%s/%%s',rtrim(%Q,'/'),ltrim(path,'/'))", conf->rootdir); } else { sqlite3_str_appendf(s, "printf('/%%s', trim(path, '/'))"); } sqlite3_str_appendall(s, " as dest from syncconflicts"); if (conflict_type) { sqlite3_str_appendf(s," where conflict = %Q", conflict_type); } if (conf->reverse) { sqlite3_str_appendall(s," order by length(path) desc, path desc,pkgid collate vercmp desc, conflict desc"); } else { sqlite3_str_appendall(s," order by length(path), path, pkgid collate vercmp, conflict"); } sql = sqlite3_str_value(s); rv = zpm_exec(conf->log, sql, callback, conf, &errmsg); sqlite3_str_finish(s); if (rv) { fprintf(stderr, "exec fail: %s\n", sqlite3_errstr(rv)); if (errmsg) { fprintf(stderr, "database error: %s\n", errmsg); conf->errors++; } if (conf->log->error == 1) { fprintf(stderr, "unable to allocate memory\n"); } fprintf(stderr, "zpm_exec failure: %s\n", conf->log->errmsg ? conf->log->errmsg : "unknown"); conf->errors++; } if (conf->log->errmsg) { fprintf(stderr, "error: %s\n", conf->log->errmsg); } if (conf->errors && conf->exitonerror) { zpm_close(conf->log); zpm_close(conf->src); exit(EXIT_FAILURE); } /* TODO final report function in conf var */ } static void runstage(struct config *conf, char *stage, int (callback)(void *, int, char **, char **)) { int rv; char *errmsg; sqlite3_str *s; char *sql; s = sqlite3_str_new(conf->log->db); sqlite3_str_appendall(s, "select *, "); if (conf->rootdir) { sqlite3_str_appendf(s, "printf('%%s/%%s',rtrim(%Q,'/'),ltrim(path,'/'))", conf->rootdir); } else { sqlite3_str_appendf(s, "printf('/%%s', trim(path, '/'))"); } sqlite3_str_appendall(s, " as dest from syncinfo"); if (stage) { sqlite3_str_appendf(s," where op = %Q", stage); } if (conf->reverse) { sqlite3_str_appendall(s," order by length(path) desc, path desc"); } else { sqlite3_str_appendall(s," order by length(path), path"); } sql = sqlite3_str_value(s); rv = zpm_exec(conf->log, sql, callback, conf, &errmsg); sqlite3_str_finish(s); if (rv) { fprintf(stderr, "exec fail: %s\n", sqlite3_errstr(rv)); if (errmsg) { fprintf(stderr, "database error: %s\n", errmsg); conf->errors++; } if (conf->log->error == 1) { fprintf(stderr, "unable to allocate memory\n"); } fprintf(stderr, "zpm_exec failure: %s\n", conf->log->errmsg ? conf->log->errmsg : "unknown"); conf->errors++; } #if 0 if (conf->log->errmsg) { fprintf(stderr, "error: %s\n", conf->log->errmsg); } #endif if (conf->errors && conf->exitonerror) { zpm_close(conf->log); zpm_close(conf->src); exit(EXIT_FAILURE); } /* TODO final report function in conf var */ } static void count_ops(struct config *conf) { conf->ops_remove = zpm_db_int(conf->log, "select count(*) from syncinfo where op = 'remove'"); conf->ops_update = zpm_db_int(conf->log, "select count(*) from syncinfo where op = 'update'"); conf->ops_install = zpm_db_int(conf->log, "select count(*) from syncinfo where op = 'new'"); conf->ops_total = conf->ops_remove + conf->ops_update + conf->ops_install; } int main(int ac, char **av) { struct zpm localdb; struct zpm pkgdb; int opt; char *pkgdbfile = 0, *localdbfile = 0; char *s; struct config conf = { 0 }; conf.errabort = 1; conf.errors = 0; conf.conflicts = 0; conf.verbose = 1; conf.dryrun = 0; conf.setuser = 1; conf.setgroup = 1; conf.log = 0; conf.src = 0; conf.rootdir = 0; conf.reverse = 0; conf.overwrite = 0; conf.accept = 0; conf.acceptdir = 1; zpm_buffer_init(&conf.note); if (geteuid() != 0) { conf.setuser = 0; conf.setgroup = 0; } localdbfile = ZPM_LOCAL_DB; if ((s = getenv("ZPMDB"))) { localdbfile = s; } if ((s = getenv("ZPM_ROOT_DIR"))) { conf.rootdir = s; } /* * -d localdb or ZPMDB * or /var/lib/zpm/zpm.db, or die * -f 'package database', otherwise regular default of env * ZPM_PACKAGE_FILE, or use pkgdb if otherwise not found * -R root of pkg, will just chdir there * * args are pkgid triple, but will do a pkg find on the pkgdb */ while ((opt = getopt(ac, av, "f:d:c:nCR:vqOAMDp")) != -1) { switch (opt) { case 'd': localdbfile = optarg; break; case 'f': pkgdbfile = optarg; break; case 'n': conf.dryrun = 1; break; case 'v': conf.verbose++; break; case 'q': conf.verbose--; break; case 'C': conf.errabort = 0; break; case 'R': conf.rootdir = optarg; break; case 'N': conf.setuser = 0; conf.setgroup = 0; break; case 'O': conf.overwrite = 1; break; case 'A': conf.accept = 1; break; case 'M': conf.ignoredirmd = 1; case 'D': conf.acceptdir = 0; case 'p': conf.progress++; default: usage(); exit(EXIT_FAILURE); break; } } /* verify root dir exists */ if (conf.rootdir && !exists(conf.rootdir, NULL)) { fprintf(stderr, "rootdir %s does not exist\n", conf.rootdir); } if (!zpm_open(&localdb, localdbfile)) { fprintf(stderr, "can't open zpm db %s\n", localdbfile); exit(EXIT_FAILURE); } conf.log = &localdb; if (pkgdbfile) { /* TODO open read-only */ if (!zpm_open(&pkgdb, pkgdbfile)) { fprintf(stderr, "can't open src db %s\n", localdbfile); exit(EXIT_FAILURE); } else { conf.src = &pkgdb; } } /* TODO set conf var to finalize error reporting */ if (conf.verbose) { fprintf(stderr, "syncing filesystem %s (ldb %s)\n", conf.rootdir ? conf.rootdir : "/", localdbfile); } conf.errors = 0; conf.exitonerror = 0; check_conflicts(&conf, NULL, report_conflicts); if (conf.conflicts) { fprintf(stderr, "%d conflicts reported, aborting sync\n", conf.conflicts); conf.errors++; } else { /* no point in running it if we're just going to * overwrite everything */ if (!conf.overwrite && !conf.accept && !conf.dryrun) { runstage(&conf, "new", check_existing); } if (conf.verbose && !conf.errors) { fprintf(stderr, "beginning %ssync\n", conf.dryrun ? "dryrun " : ""); } if (!conf.errors) { handle_config_files(&conf); } /* have to do the removes first otherwise * old files may conflict with update file * type changes */ if (!conf.errors) { conf.exitonerror = conf.dryrun ? 0 : 1; conf.errabort = conf.dryrun ? 0 : 1; count_ops(&conf); fprintf(stderr, "file ops: %lu\n", conf.ops_total); if (conf.ops_remove > 0) { if (conf.verbose) { fprintf(stderr, "removing %lu file%s\n", conf.ops_remove, conf.ops_remove > 1 ? "s" : ""); } conf.reverse = 1; conf.ops_completed = 0; conf.ops_total = conf.ops_remove; runstage(&conf, "remove", remove_files); if (conf.verbose && conf.progress < 2) { fprintf(stderr, " done\n"); fflush(stderr); } } if (conf.ops_update > 0) { if (conf.verbose) { fprintf(stderr, "updating %lu file%s\n", conf.ops_update, conf.ops_update > 1 ? "s" : ""); } conf.reverse = 0; conf.ops_completed = 0; conf.ops_total = conf.ops_update; runstage(&conf, "update", install_files); if (conf.verbose && conf.progress < 2) { fprintf(stderr, " done\n"); fflush(stderr); } } if (conf.ops_install > 0) { if (conf.verbose) { fprintf(stderr, "installing %lu file%s\n", conf.ops_install, conf.ops_install > 1 ? "s" : ""); } conf.reverse = 0; conf.ops_completed = 0; conf.ops_total = conf.ops_install; runstage(&conf, "new", install_files); if (conf.verbose && conf.progress < 2) { fprintf(stderr, " done\n"); fflush(stderr); } } } } zpm_close(&localdb); zpm_close(conf.src); /* TODO add the note */ zpm_buffer_free(&conf.note); return conf.errors ? 1 : 0; }