diff options
author | Przemyslaw Pawelczyk <przemoc@gmail.com> | 2015-09-13 22:12:14 +0200 |
---|---|---|
committer | Przemyslaw Pawelczyk <przemoc@gmail.com> | 2015-09-13 22:12:14 +0200 |
commit | 16ab153f3a54194e3217fcf1a235904e4b61623b (patch) | |
tree | c27f9326815ebf4454e753028dcb3d388b3c0990 /src/metastore.c | |
parent | 295428a550bb55a509ce15c5e27a0ab173799ebb (diff) |
Move source files to src/ directory.
As a bonus you can build out-of-tree now via make -f.
It's part of the work related to issue #22.
Diffstat (limited to 'src/metastore.c')
-rw-r--r-- | src/metastore.c | 539 |
1 files changed, 539 insertions, 0 deletions
diff --git a/src/metastore.c b/src/metastore.c new file mode 100644 index 0000000..2956cab --- /dev/null +++ b/src/metastore.c @@ -0,0 +1,539 @@ +/* + * Main functions of the program. + * + * Copyright (C) 2007 David Härdeman <david@hardeman.nu> + * + * 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; only version 2 of the License is applicable. + * + * 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; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define _BSD_SOURCE +#include <sys/types.h> +#include <sys/stat.h> +#include <getopt.h> +#include <utime.h> +#include <sys/xattr.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> + +#include "metastore.h" +#include "settings.h" +#include "utils.h" +#include "metaentry.h" + +/* metastore settings */ +static struct metasettings settings = { + .metafile = METAFILE, + .do_mtime = false, + .do_emptydirs = false, + .do_removeemptydirs = false, + .do_git = false, +}; + +/* Used to create lists of dirs / other files which are missing in the fs */ +static struct metaentry *missingdirs = NULL; +static struct metaentry *missingothers = NULL; + +/* Used to create lists of dirs / other files which are missing in metadata */ +static struct metaentry *extradirs = NULL; + +/* + * Inserts an entry in a linked list ordered by pathlen + */ +static void +insert_entry_plist(struct metaentry **list, struct metaentry *entry) +{ + struct metaentry **parent; + + for (parent = list; *parent; parent = &((*parent)->list)) { + if ((*parent)->pathlen > entry->pathlen) + break; + } + + entry->list = *parent; + *parent = entry; +} + +/* + * Inserts an entry in a linked list ordered by pathlen descendingly + */ +static void +insert_entry_pdlist(struct metaentry **list, struct metaentry *entry) +{ + struct metaentry **parent; + + for (parent = list; *parent; parent = &((*parent)->list)) { + if ((*parent)->pathlen < entry->pathlen) + break; + } + + entry->list = *parent; + *parent = entry; +} + +/* + * Prints differences between real and stored actual metadata + * - for use in mentries_compare + */ +static void +compare_print(struct metaentry *real, struct metaentry *stored, int cmp) +{ + if (!real && !stored) { + msg(MSG_ERROR, "%s called with incorrect arguments\n", __FUNCTION__); + return; + } + + if (cmp == DIFF_NONE) { + msg(MSG_DEBUG, "%s:\tno difference\n", real->path); + return; + } + + msg(MSG_QUIET, "%s:\t", real ? real->path : stored->path); + + if (cmp & DIFF_ADDED) + msg(MSG_QUIET, "added ", real->path); + if (cmp & DIFF_DELE) + msg(MSG_QUIET, "removed ", stored->path); + if (cmp & DIFF_OWNER) + msg(MSG_QUIET, "owner "); + if (cmp & DIFF_GROUP) + msg(MSG_QUIET, "group "); + if (cmp & DIFF_MODE) + msg(MSG_QUIET, "mode "); + if (cmp & DIFF_TYPE) + msg(MSG_QUIET, "type "); + if (cmp & DIFF_MTIME) + msg(MSG_QUIET, "mtime "); + if (cmp & DIFF_XATTR) + msg(MSG_QUIET, "xattr "); + msg(MSG_QUIET, "\n"); +} + +/* + * Tries to change the real metadata to match the stored one + * - for use in mentries_compare + */ +static void +compare_fix(struct metaentry *real, struct metaentry *stored, int cmp) +{ + struct group *group; + struct passwd *owner; + gid_t gid = -1; + uid_t uid = -1; + struct utimbuf tbuf; + unsigned i; + + if (!real && !stored) { + msg(MSG_ERROR, "%s called with incorrect arguments\n", + __FUNCTION__); + return; + } + + if (!real) { + if (S_ISDIR(stored->mode)) + insert_entry_plist(&missingdirs, stored); + else + insert_entry_plist(&missingothers, stored); + + msg(MSG_NORMAL, "%s:\tremoved\n", stored->path); + return; + } + + if (!stored) { + if (S_ISDIR(real->mode)) + insert_entry_pdlist(&extradirs, real); + msg(MSG_NORMAL, "%s:\tadded\n", real->path); + return; + } + + if (cmp == DIFF_NONE) { + msg(MSG_DEBUG, "%s:\tno difference\n", real->path); + return; + } + + if (cmp & DIFF_TYPE) { + msg(MSG_NORMAL, "%s:\tnew type, will not change metadata\n", + real->path); + return; + } + + msg(MSG_QUIET, "%s:\tchanging metadata\n", real->path); + + while (cmp & (DIFF_OWNER | DIFF_GROUP)) { + if (cmp & DIFF_OWNER) { + msg(MSG_NORMAL, "%s:\tchanging owner from %s to %s\n", + real->path, real->group, stored->group); + owner = xgetpwnam(stored->owner); + if (!owner) { + msg(MSG_DEBUG, "\tgetpwnam failed: %s\n", + strerror(errno)); + break; + } + uid = owner->pw_uid; + } + + if (cmp & DIFF_GROUP) { + msg(MSG_NORMAL, "%s:\tchanging group from %s to %s\n", + real->path, real->group, stored->group); + group = xgetgrnam(stored->group); + if (!group) { + msg(MSG_DEBUG, "\tgetgrnam failed: %s\n", + strerror(errno)); + break; + } + gid = group->gr_gid; + } + + if (lchown(real->path, uid, gid)) { + msg(MSG_DEBUG, "\tlchown failed: %s\n", + strerror(errno)); + break; + } + break; + } + + if (cmp & DIFF_MODE) { + msg(MSG_NORMAL, "%s:\tchanging mode from 0%o to 0%o\n", + real->path, real->mode & 07777, stored->mode & 07777); + if (chmod(real->path, stored->mode & 07777)) + msg(MSG_DEBUG, "\tchmod failed: %s\n", strerror(errno)); + } + + /* FIXME: Use utimensat here, or even better - lutimensat */ + if ((cmp & DIFF_MTIME) && S_ISLNK(real->mode)) { + msg(MSG_NORMAL, "%s:\tsymlink, not changing mtime\n", real->path); + } else if (cmp & DIFF_MTIME) { + msg(MSG_NORMAL, "%s:\tchanging mtime from %ld to %ld\n", + real->path, real->mtime, stored->mtime); + tbuf.actime = stored->mtime; + tbuf.modtime = stored->mtime; + if (utime(real->path, &tbuf)) { + msg(MSG_DEBUG, "\tutime failed: %s\n", strerror(errno)); + return; + } + } + + if (cmp & DIFF_XATTR) { + for (i = 0; i < real->xattrs; i++) { + /* Any attrs to remove? */ + if (mentry_find_xattr(stored, real, i) >= 0) + continue; + + msg(MSG_NORMAL, "%s:\tremoving xattr %s\n", + real->path, real->xattr_names[i]); + if (lremovexattr(real->path, real->xattr_names[i])) + msg(MSG_DEBUG, "\tlremovexattr failed: %s\n", + strerror(errno)); + } + + for (i = 0; i < stored->xattrs; i++) { + /* Any xattrs to add? (on change they are removed above) */ + if (mentry_find_xattr(real, stored, i) >= 0) + continue; + + msg(MSG_NORMAL, "%s:\tadding xattr %s\n", + stored->path, stored->xattr_names[i]); + if (lsetxattr(stored->path, stored->xattr_names[i], + stored->xattr_values[i], + stored->xattr_lvalues[i], XATTR_CREATE)) + msg(MSG_DEBUG, "\tlsetxattr failed: %s\n", + strerror(errno)); + } + } +} + +/* + * Tries to fix any empty dirs which are missing from the filesystem by + * recreating them. + */ +static void +fixup_emptydirs(void) +{ + struct metaentry *entry; + struct metaentry *cur; + struct metaentry **parent; + char *bpath; + char *delim; + size_t blen; + struct metaentry *new; + + if (!missingdirs) + return; + msg(MSG_DEBUG, "\nAttempting to recreate missing dirs\n"); + + /* If directory x/y is missing, but file x/y/z is also missing, + * we should prune directory x/y from the list of directories to + * recreate since the deletition of x/y is likely to be genuine + * (as opposed to empty dir pruning like git/cvs does). + * + * Also, if file x/y/z is missing, any child directories of + * x/y should be pruned as they are probably also intentionally + * removed. + */ + + msg(MSG_DEBUG, "List of candidate dirs:\n"); + for (cur = missingdirs; cur; cur = cur->list) + msg(MSG_DEBUG, " %s\n", cur->path); + + for (entry = missingothers; entry; entry = entry->list) { + msg(MSG_DEBUG, "Pruning using file %s\n", entry->path); + bpath = xstrdup(entry->path); + delim = strrchr(bpath, '/'); + if (!delim) { + msg(MSG_NORMAL, "No delimiter found in %s\n", bpath); + free(bpath); + continue; + } + *delim = '\0'; + + parent = &missingdirs; + for (cur = *parent; cur; cur = cur->list) { + if (strcmp(cur->path, bpath)) { + parent = &cur->list; + continue; + } + + msg(MSG_DEBUG, "Prune phase 1 - %s\n", cur->path); + *parent = cur->list; + } + + /* Now also prune subdirs of the base dir */ + *delim++ = '/'; + *delim = '\0'; + blen = strlen(bpath); + + parent = &missingdirs; + for (cur = *parent; cur; cur = cur->list) { + if (strncmp(cur->path, bpath, blen)) { + parent = &cur->list; + continue; + } + + msg(MSG_DEBUG, "Prune phase 2 - %s\n", cur->path); + *parent = cur->list; + } + + free(bpath); + } + msg(MSG_DEBUG, "\n"); + + for (cur = missingdirs; cur; cur = cur->list) { + msg(MSG_QUIET, "%s:\trecreating...", cur->path); + if (mkdir(cur->path, cur->mode)) { + msg(MSG_QUIET, "failed (%s)\n", strerror(errno)); + continue; + } + msg(MSG_QUIET, "ok\n"); + + new = mentry_create(cur->path); + if (!new) { + msg(MSG_QUIET, "Failed to get metadata for %s\n"); + continue; + } + + compare_fix(new, cur, mentry_compare(new, cur, &settings)); + } +} + +/* + * Deletes any empty dirs present in the filesystem that are missing + * from the metadata. + * An "empty" dir is one which either: + * - is empty; or + * - only contains empty dirs + */ +static void +fixup_newemptydirs(void) +{ + struct metaentry **cur; + int removed_dirs = 1; + + if (!extradirs) + return; + + /* This is a simpleminded algorithm that attempts to rmdir() all + * directories discovered missing from the metadata. Naturally, this will + * succeed only on the truly empty directories, but depending on the order, + * it may mean that parent directory removal are attempted to be removed + * *before* the children. To circumvent this, keep looping around all the + * directories until none have been successfully removed. This is a + * O(N**2) algorithm, so don't try to remove too many nested directories + * at once (e.g. thousands). + * + * Note that this will succeed only if each parent directory is writable. + */ + while (removed_dirs) { + removed_dirs = 0; + msg(MSG_DEBUG, "\nAttempting to delete empty dirs\n"); + for (cur = &extradirs; *cur;) { + msg(MSG_QUIET, "%s:\tremoving...", (*cur)->path); + if (rmdir((*cur)->path)) { + msg(MSG_QUIET, "failed (%s)\n", strerror(errno)); + cur = &(*cur)->list; + continue; + } + /* No freeing, because OS will do the job at the end. */ + *cur = (*cur)->list; + removed_dirs++; + msg(MSG_QUIET, "ok\n"); + } + } +} + +/* Prints usage message and exits */ +static void +usage(const char *arg0, const char *message) +{ + if (message) + msg(MSG_CRITICAL, "%s: %s\n\n", arg0, message); + msg(MSG_CRITICAL, +"Usage: %s ACTION [OPTION...] [PATH...]\n", + arg0); + msg(MSG_CRITICAL, +"\n" +"Where ACTION is one of:\n" +" -c, --compare Show differences between stored and real metadata\n" +" -s, --save Save current metadata\n" +" -a, --apply Apply stored metadata\n" +" -d, --dump Dump stored (if no PATH is given) or real metadata\n" +" (if PATH is present, e.g. ./) in human-readable form\n" +" -h, --help Help message (this text)\n" +"\n" +"Valid OPTIONS are:\n" +" -v, --verbose Print more verbose messages\n" +" -q, --quiet Print less verbose messages\n" +" -m, --mtime Also take mtime into account for diff or apply\n" +" -e, --empty-dirs Recreate missing empty directories\n" +" -E, --remove-empty-dirs Remove extra empty directories\n" +" -g, --git Do not omit .git directories\n" +" -f, --file=FILE Set metadata file (" METAFILE " by default)\n" + ); + + exit(message ? EXIT_FAILURE : EXIT_SUCCESS); +} + +/* Options */ +static struct option long_options[] = { + { "compare", no_argument, NULL, 'c' }, + { "save", no_argument, NULL, 's' }, + { "apply", no_argument, NULL, 'a' }, + { "dump", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + { "verbose", no_argument, NULL, 'v' }, + { "quiet", no_argument, NULL, 'q' }, + { "mtime", no_argument, NULL, 'm' }, + { "empty-dirs", no_argument, NULL, 'e' }, + { "remove-empty-dirs", no_argument, NULL, 'E' }, + { "git", no_argument, NULL, 'g' }, + { "file", required_argument, NULL, 'f' }, + { NULL, 0, NULL, 0 } +}; + +/* Main function */ +int +main(int argc, char **argv) +{ + int i, c; + struct metahash *real = NULL; + struct metahash *stored = NULL; + int action = 0; + + /* Parse options */ + i = 0; + while (1) { + int option_index = 0; + c = getopt_long(argc, argv, "csadhvqmeEgf:", + long_options, &option_index); + if (c == -1) + break; + switch (c) { + case 'c': /* compare */ action |= ACTION_DIFF; i++; break; + case 's': /* save */ action |= ACTION_SAVE; i++; break; + case 'a': /* apply */ action |= ACTION_APPLY; i++; break; + case 'd': /* dump */ action |= ACTION_DUMP; i++; break; + case 'h': /* help */ action |= ACTION_HELP; i++; break; + case 'v': /* verbose */ adjust_verbosity(1); break; + case 'q': /* quiet */ adjust_verbosity(-1); break; + case 'm': /* mtime */ settings.do_mtime = true; break; + case 'e': /* empty-dirs */ settings.do_emptydirs = true; break; + case 'E': /* remove-empty-dirs */ settings.do_removeemptydirs = true; + break; + case 'g': /* git */ settings.do_git = true; break; + case 'f': /* file */ settings.metafile = optarg; break; + default: + usage(argv[0], "unknown option"); + } + } + + /* Make sure only one action is specified */ + if (i != 1) + usage(argv[0], "incorrect option(s)"); + + /* Make sure --empty-dirs is only used with apply */ + if (settings.do_emptydirs && action != ACTION_APPLY) + usage(argv[0], "--empty-dirs is only valid with --apply"); + + /* Make sure --remove-empty-dirs is only used with apply */ + if (settings.do_removeemptydirs && action != ACTION_APPLY) + usage(argv[0], "--remove-empty-dirs is only valid with --apply"); + + if (action == ACTION_HELP) + usage(argv[0], NULL); + + /* Perform action */ + if (action & ACTIONS_READING && !(action == ACTION_DUMP && optind < argc)) { + mentries_fromfile(&stored, settings.metafile); + if (!stored) { + msg(MSG_CRITICAL, "Failed to load metadata from %s\n", + settings.metafile); + exit(EXIT_FAILURE); + } + } + + if (optind < argc) { + while (optind < argc) + mentries_recurse_path(argv[optind++], &real, &settings); + } else if (action != ACTION_DUMP) { + mentries_recurse_path(".", &real, &settings); + } + + if (!real && (action != ACTION_DUMP || optind < argc)) { + msg(MSG_CRITICAL, + "Failed to load metadata from file system\n"); + exit(EXIT_FAILURE); + } + + switch (action) { + case ACTION_DIFF: + mentries_compare(real, stored, compare_print, &settings); + break; + case ACTION_SAVE: + mentries_tofile(real, settings.metafile); + break; + case ACTION_APPLY: + mentries_compare(real, stored, compare_fix, &settings); + if (settings.do_emptydirs) + fixup_emptydirs(); + if (settings.do_removeemptydirs) + fixup_newemptydirs(); + break; + case ACTION_DUMP: + mentries_dump(real ? real : stored); + break; + } + + exit(EXIT_SUCCESS); +} + |