#define _POSIX_C_SOURCE 200809L #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" 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, absorb; }; static void usage() { printf("usage: zpm $scriptname [-fncC] args ...\n"); } 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 */ if (mkdir(pcopy, 0755) == -1) { switch (errno) { case EEXIST: if (lstat(pcopy, &st) == -1) { /* can't stat? */ return 0; } switch (st.st_mode & S_IFMT) { case S_IFDIR: case S_IFLNK: break; default: return 0; } break; default: return 0; } } if (delim) { *delim = ch; } s = delim; } while (delim); return 1; } 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) 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 check_existing(void *f, int ncols, char **vals, char **cols) { struct config *conf = f; char *path; struct stat st; path = COL("dest"); if (!path) { return seterror(conf, "no path"); } if (conf->dryrun) { printf("checkfor %s\n", path); return 0; } if (conf->verbose) { fprintf(stderr, "check for existing %s\n", path); } if (lstat(path, &st) == 0) { fprintf(stderr, "%s exists\n", path); conf->errors++; } else { switch(errno) { /* not an error, file shouldn't exist*/ case ENOENT: break; default: fprintf(stderr, "unable to check %s: %s\n", path, strerror(errno)); conf->errors++; break; } } return 0; } static int remove_files(void *f, int ncols, char **vals, char **cols) { struct config *conf = f; char *dest; struct stat st; dest = COL("dest"); if (!dest) return seterror(conf,"no file dest"); if (conf->dryrun) { char *ftype = COL("filetype"); int t = *ftype; switch(t) { case 'd': printf("rmdir %s\n", dest); break; default: printf("unlink %s\n", dest); break; } return 0; } if (lstat(dest, &st) == -1) { return seterror(conf,"can't stat"); } if (S_ISDIR(st.st_mode)) { if (conf->verbose) { fprintf(stderr, "rmdir(%s)\n", dest); } rmdir(dest); } else if (S_ISREG(st.st_mode)) { /* TODO conf to import before removal */ if (conf->verbose) { fprintf(stderr, "unlink(%s)\n", dest); } if (unlink(dest) == -1) { return seterror(conf, "can't unlink"); } } else { if (unlink(dest) == -1) { return seterror(conf, "can't unlink"); } } return 0; } #define MARK fprintf(stderr, "%s %d: mark\n", __func__, __LINE__) struct nitem { int op; uid_t uid; gid_t gid; char *dest; char *path; char *hash; char *target; time_t mtime; mode_t mode; int ftype; struct timespec times[2]; }; #define D_NOEXIST 0x1 #define D_TYPE 0x2 #define D_MD 0x4 #define D_HASH 0x8 #define D_ISDIR 0x10 #define D_EISDIR 0x20 #define D_UID 0x40 #define D_GID 0x80 #define D_MODE 0x100 #define D_MTIME 0x200 #define D_ERROR 0x1000 #define D_STATERROR 0x2000 #define D_RLERROR 0x4000 /* 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 (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->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 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; } static int read_item(struct config *conf, int ncols, char **vals, char **cols, struct nitem *n) { char *val; 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->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("filetype"); if (!val || strlen(val) == 0) { seterror(conf, "can't determine file type"); return 0; } n->ftype = *val; 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"); return 0; } n->gid = gr->gr_gid; } else { n->gid = getegid(); } double mtime = strtod(COL("mtime"),NULL); 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; } 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; rv = chmod(item->path, item->mode); if (rv == -1) { setsyserr(conf, "can't chmod"); return conf->errabort; } if (conf->setuser && conf->setgroup) { rv = chown(item->path, item->uid, item->gid); if (rv == -1) { setsyserr(conf, "can't chown %s", 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"); 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 */ 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 & 16) { failure = 0; success = 1; } source = conf->src ? conf->src : conf->log; if (unlink_file) { rv = remove_existing(conf, item->dest); } else if (rm_dir) { rv = remove_dir(conf, item->dest); } if (rv != 1) { return failure; } if (mkleading) { rv = create_leading_dirs(item->dest); if (!rv) { setsyserr(conf, "can't create leading dirs for %s", item->dest); return failure; } } if (item->ftype == 'r') { rv = zpm_extract(source, item->hash, item->dest, item->mode); if (rv == 0) { seterror(conf, "can't extract %s", item->dest); return failure; } return success; } switch (item->ftype) { case 'd': rv = mkdir(item->dest, item->mode); break; case 'l': rv = symlink(item->target, item->dest); break; default: /* error */ break; } if (rv == -1) { setsyserr(conf, "installing %s failed", item->dest); return failure; } if (setmd) { return set_md(conf, item) == 0 ? success : failure; } return success; } 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; /* TODO 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 (conf->verbose) { fprintf(stderr, "%d '%c' %s\n", nitem.op, nitem.ftype, nitem.dest); } if (conf->dryrun) { printf("new %c%o %d:%d %s -> %s\n", nitem.ftype, nitem.mode, nitem.uid, nitem.gid, nitem.path, nitem.dest); return 0; } unsigned int diffs = file_compare(&nitem, &existing); if (diffs >= D_ERROR) { return seterror(conf, "can't check %s", nitem.dest); } if (conf->verbose) { fprintf(stderr, "diffs = %u\n", diffs); } /* 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 isdir = (diffs & D_ISDIR); int eisdir = (diffs & D_EISDIR); int accept = conf->absorb; int overwrite = conf->overwrite; int installing = (nitem.op == OP_NEW); update = (nitem.op == OP_UPDATE); if (update) { if (!exist) { /* warn, it should exist */ fprintf(stderr, "%s missing, installing", nitem.dest); return install(conf, &nitem, 3); } /* file exists in filesystem */ if (sametype) { if (mdsame && hashsame) { fprintf(stderr, "%s should not be an update", nitem.dest); /* warn, bug in logic. This shouldn't * occur, because if there is nothing * to do, it shouldn't be listed * as an update */ /* do nothing */ return 0; } if (!mdsame && hashsame) { /* fix md */ return set_md(conf, &nitem); } if (mdsame && !hashsame) { /* install */ return install(conf, &nitem, 3); } if (!mdsame && !hashsame) { /* install */ return install(conf, &nitem, 3); } } /* 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)? */ } if (installing) { if (!exist) { return install(conf, &nitem, 3); } /* file exists in filesystem */ if (sametype) { if (mdsame && hashsame && (accept || overwrite)) { /* do nothing */ return 0; } if (mdsame && hashsame && !(accept || overwrite)) { /* error */ return seterror(conf, "bad conditions"); } 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, accept ? "existing file not acceptable" : "file exists"); } 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, accept ? "existing file not acceptable" : "file exists"); } 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, accept ? "existing file not acceptable" : "file exists"); } /* TODO error, should be impossible */ return seterror(conf, "impossible state reached"); } /* file exists, and is not the same type */ if (!overwrite) { /* error */ return seterror(conf, accept ? "existing file not acceptable" : "file exists"); } /* 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 * TODO */ 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 */ /* TODO error */ return seterror(conf, "impossible state 2 reached"); } 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); if (conf->verbose > 2) { fprintf(stderr, "stage query: %s\n", sql); } 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"); } sql = sqlite3_str_value(s); if (conf->verbose > 2) { fprintf(stderr, "stage query: %s\n", sql); } 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 */ } int main(int ac, char **av){ struct zpm localdb; struct zpm pkgdb; int opt; char *pkgdbfile = 0, *localdbfile = 0; char *s; struct config conf; conf.errabort = 1; conf.errors = 0; conf.conflicts = 0; conf.verbose = 0; 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.absorb = 0; if (geteuid() != 0) { conf.setuser = 0; conf.setgroup = 0; } localdbfile = ZPM_LOCAL_DB; if ((s = getenv("ZPMDB"))) { /* TODO does this need to be copied ? */ localdbfile = s; } if ((s = getenv("ZPM_ROOT_DIR"))) { /* TODO does this need to be copied ? */ 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:vOA")) != -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 '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.absorb = 1; break; 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) { if (!zpm_open(&pkgdb, pkgdbfile)) { fprintf(stderr, "can't open src db %s\n", localdbfile); exit(EXIT_FAILURE); } else { conf.src = &pkgdb; } } /* TODO find pkgid from arg */ /* TODO set conf var to finalize error reporting */ if (conf.verbose) { fprintf(stderr, "syncing filesystem %s (ldb %s) from %s\n", conf.rootdir ? conf.rootdir : "/", localdbfile, pkgdbfile); } 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 == 0 || conf.absorb == 0) { runstage(&conf, "new", check_existing); } if (conf.verbose) { fprintf(stderr, "beginning sync\n"); } /* have to do the removes first otherwise * old files may conflict with update file * type changes */ if (!conf.errors) { conf.exitonerror = 1; conf.reverse = 1; if (conf.verbose) { fprintf(stderr, "removing old files\n"); } runstage(&conf, "remove", remove_files); conf.reverse = 0; if (conf.verbose) { fprintf(stderr, "updating files\n"); } runstage(&conf, "update", install_files); if (conf.verbose) { fprintf(stderr, "installing files\n"); } runstage(&conf, "new", install_files); } } zpm_close(&localdb); zpm_close(conf.src); return conf.errors ? 1 : 0; }