Merge branch 'impro/switch-xattr-header'
[metastore.git] / metastore.c
blob38901de8175b18e06c95ae31420e85c1a1d2aa6e
1 /*
2 * Main functions of the program.
4 * Copyright (C) 2007 David Härdeman <david@hardeman.nu>
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation version 2 of the License.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #define _BSD_SOURCE
22 #include <sys/types.h>
23 #include <sys/stat.h>
24 #include <getopt.h>
25 #include <utime.h>
26 #include <sys/xattr.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <unistd.h>
30 #include <errno.h>
32 #include "metastore.h"
33 #include "settings.h"
34 #include "utils.h"
35 #include "metaentry.h"
37 /* metastore settings */
38 static struct metasettings settings = {
39 .metafile = METAFILE,
40 .do_mtime = false,
41 .do_emptydirs = false,
42 .do_removeemptydirs = false,
43 .do_git = false,
46 /* Used to create lists of dirs / other files which are missing in the fs */
47 static struct metaentry *missingdirs = NULL;
48 static struct metaentry *missingothers = NULL;
50 /* Used to create lists of dirs / other files which are missing in metadata */
51 static struct metaentry *extradirs = NULL;
54 * Inserts an entry in a linked list ordered by pathlen
56 static void
57 insert_entry_plist(struct metaentry **list, struct metaentry *entry)
59 struct metaentry **parent;
61 for (parent = list; *parent; parent = &((*parent)->list)) {
62 if ((*parent)->pathlen > entry->pathlen)
63 break;
66 entry->list = *parent;
67 *parent = entry;
71 * Inserts an entry in a linked list ordered by pathlen descendingly
73 static void
74 insert_entry_pdlist(struct metaentry **list, struct metaentry *entry)
76 struct metaentry **parent;
78 for (parent = list; *parent; parent = &((*parent)->list)) {
79 if ((*parent)->pathlen < entry->pathlen)
80 break;
83 entry->list = *parent;
84 *parent = entry;
88 * Prints differences between real and stored actual metadata
89 * - for use in mentries_compare
91 static void
92 compare_print(struct metaentry *real, struct metaentry *stored, int cmp)
94 if (!real && !stored) {
95 msg(MSG_ERROR, "%s called with incorrect arguments\n", __FUNCTION__);
96 return;
99 if (cmp == DIFF_NONE) {
100 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
101 return;
104 msg(MSG_QUIET, "%s:\t", real ? real->path : stored->path);
106 if (cmp & DIFF_ADDED)
107 msg(MSG_QUIET, "added ", real->path);
108 if (cmp & DIFF_DELE)
109 msg(MSG_QUIET, "removed ", stored->path);
110 if (cmp & DIFF_OWNER)
111 msg(MSG_QUIET, "owner ");
112 if (cmp & DIFF_GROUP)
113 msg(MSG_QUIET, "group ");
114 if (cmp & DIFF_MODE)
115 msg(MSG_QUIET, "mode ");
116 if (cmp & DIFF_TYPE)
117 msg(MSG_QUIET, "type ");
118 if (cmp & DIFF_MTIME)
119 msg(MSG_QUIET, "mtime ");
120 if (cmp & DIFF_XATTR)
121 msg(MSG_QUIET, "xattr ");
122 msg(MSG_QUIET, "\n");
126 * Tries to change the real metadata to match the stored one
127 * - for use in mentries_compare
129 static void
130 compare_fix(struct metaentry *real, struct metaentry *stored, int cmp)
132 struct group *group;
133 struct passwd *owner;
134 gid_t gid = -1;
135 uid_t uid = -1;
136 struct utimbuf tbuf;
137 unsigned i;
139 if (!real && !stored) {
140 msg(MSG_ERROR, "%s called with incorrect arguments\n",
141 __FUNCTION__);
142 return;
145 if (!real) {
146 if (S_ISDIR(stored->mode))
147 insert_entry_plist(&missingdirs, stored);
148 else
149 insert_entry_plist(&missingothers, stored);
151 msg(MSG_NORMAL, "%s:\tremoved\n", stored->path);
152 return;
155 if (!stored) {
156 if (S_ISDIR(real->mode))
157 insert_entry_pdlist(&extradirs, real);
158 msg(MSG_NORMAL, "%s:\tadded\n", real->path);
159 return;
162 if (cmp == DIFF_NONE) {
163 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
164 return;
167 if (cmp & DIFF_TYPE) {
168 msg(MSG_NORMAL, "%s:\tnew type, will not change metadata\n",
169 real->path);
170 return;
173 msg(MSG_QUIET, "%s:\tchanging metadata\n", real->path);
175 while (cmp & (DIFF_OWNER | DIFF_GROUP)) {
176 if (cmp & DIFF_OWNER) {
177 msg(MSG_NORMAL, "%s:\tchanging owner from %s to %s\n",
178 real->path, real->group, stored->group);
179 owner = xgetpwnam(stored->owner);
180 if (!owner) {
181 msg(MSG_DEBUG, "\tgetpwnam failed: %s\n",
182 strerror(errno));
183 break;
185 uid = owner->pw_uid;
188 if (cmp & DIFF_GROUP) {
189 msg(MSG_NORMAL, "%s:\tchanging group from %s to %s\n",
190 real->path, real->group, stored->group);
191 group = xgetgrnam(stored->group);
192 if (!group) {
193 msg(MSG_DEBUG, "\tgetgrnam failed: %s\n",
194 strerror(errno));
195 break;
197 gid = group->gr_gid;
200 if (lchown(real->path, uid, gid)) {
201 msg(MSG_DEBUG, "\tlchown failed: %s\n",
202 strerror(errno));
203 break;
205 break;
208 if (cmp & DIFF_MODE) {
209 msg(MSG_NORMAL, "%s:\tchanging mode from 0%o to 0%o\n",
210 real->path, real->mode & 07777, stored->mode & 07777);
211 if (chmod(real->path, stored->mode & 07777))
212 msg(MSG_DEBUG, "\tchmod failed: %s\n", strerror(errno));
215 /* FIXME: Use utimensat here, or even better - lutimensat */
216 if ((cmp & DIFF_MTIME) && S_ISLNK(real->mode)) {
217 msg(MSG_NORMAL, "%s:\tsymlink, not changing mtime\n", real->path);
218 } else if (cmp & DIFF_MTIME) {
219 msg(MSG_NORMAL, "%s:\tchanging mtime from %ld to %ld\n",
220 real->path, real->mtime, stored->mtime);
221 tbuf.actime = stored->mtime;
222 tbuf.modtime = stored->mtime;
223 if (utime(real->path, &tbuf)) {
224 msg(MSG_DEBUG, "\tutime failed: %s\n", strerror(errno));
225 return;
229 if (cmp & DIFF_XATTR) {
230 for (i = 0; i < real->xattrs; i++) {
231 /* Any attrs to remove? */
232 if (mentry_find_xattr(stored, real, i) >= 0)
233 continue;
235 msg(MSG_NORMAL, "%s:\tremoving xattr %s\n",
236 real->path, real->xattr_names[i]);
237 if (lremovexattr(real->path, real->xattr_names[i]))
238 msg(MSG_DEBUG, "\tlremovexattr failed: %s\n",
239 strerror(errno));
242 for (i = 0; i < stored->xattrs; i++) {
243 /* Any xattrs to add? (on change they are removed above) */
244 if (mentry_find_xattr(real, stored, i) >= 0)
245 continue;
247 msg(MSG_NORMAL, "%s:\tadding xattr %s\n",
248 stored->path, stored->xattr_names[i]);
249 if (lsetxattr(stored->path, stored->xattr_names[i],
250 stored->xattr_values[i],
251 stored->xattr_lvalues[i], XATTR_CREATE))
252 msg(MSG_DEBUG, "\tlsetxattr failed: %s\n",
253 strerror(errno));
259 * Tries to fix any empty dirs which are missing from the filesystem by
260 * recreating them.
262 static void
263 fixup_emptydirs(struct metahash *real, struct metahash *stored)
265 struct metaentry *entry;
266 struct metaentry *cur;
267 struct metaentry **parent;
268 char *bpath;
269 char *delim;
270 size_t blen;
271 struct metaentry *new;
273 if (!missingdirs)
274 return;
275 msg(MSG_DEBUG, "\nAttempting to recreate missing dirs\n");
277 /* If directory x/y is missing, but file x/y/z is also missing,
278 * we should prune directory x/y from the list of directories to
279 * recreate since the deletition of x/y is likely to be genuine
280 * (as opposed to empty dir pruning like git/cvs does).
282 * Also, if file x/y/z is missing, any child directories of
283 * x/y should be pruned as they are probably also intentionally
284 * removed.
287 msg(MSG_DEBUG, "List of candidate dirs:\n");
288 for (cur = missingdirs; cur; cur = cur->list)
289 msg(MSG_DEBUG, " %s\n", cur->path);
291 for (entry = missingothers; entry; entry = entry->list) {
292 msg(MSG_DEBUG, "Pruning using file %s\n", entry->path);
293 bpath = xstrdup(entry->path);
294 delim = strrchr(bpath, '/');
295 if (!delim) {
296 msg(MSG_NORMAL, "No delimiter found in %s\n", bpath);
297 free(bpath);
298 continue;
300 *delim = '\0';
302 parent = &missingdirs;
303 for (cur = *parent; cur; cur = cur->list) {
304 if (strcmp(cur->path, bpath)) {
305 parent = &cur->list;
306 continue;
309 msg(MSG_DEBUG, "Prune phase 1 - %s\n", cur->path);
310 *parent = cur->list;
313 /* Now also prune subdirs of the base dir */
314 *delim++ = '/';
315 *delim = '\0';
316 blen = strlen(bpath);
318 parent = &missingdirs;
319 for (cur = *parent; cur; cur = cur->list) {
320 if (strncmp(cur->path, bpath, blen)) {
321 parent = &cur->list;
322 continue;
325 msg(MSG_DEBUG, "Prune phase 2 - %s\n", cur->path);
326 *parent = cur->list;
329 free(bpath);
331 msg(MSG_DEBUG, "\n");
333 for (cur = missingdirs; cur; cur = cur->list) {
334 msg(MSG_QUIET, "%s:\trecreating...", cur->path);
335 if (mkdir(cur->path, cur->mode)) {
336 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
337 continue;
339 msg(MSG_QUIET, "ok\n");
341 new = mentry_create(cur->path);
342 if (!new) {
343 msg(MSG_QUIET, "Failed to get metadata for %s\n");
344 continue;
347 compare_fix(new, cur, mentry_compare(new, cur, &settings));
352 * Deletes any empty dirs present in the filesystem that are missing
353 * from the metadata.
354 * An "empty" dir is one which either:
355 * - is empty; or
356 * - only contains empty dirs
358 static void
359 fixup_newemptydirs(void)
361 struct metaentry **cur;
362 int removed_dirs = 1;
364 if (!extradirs)
365 return;
367 /* This is a simpleminded algorithm that attempts to rmdir() all
368 * directories discovered missing from the metadata. Naturally, this will
369 * succeed only on the truly empty directories, but depending on the order,
370 * it may mean that parent directory removal are attempted to be removed
371 * *before* the children. To circumvent this, keep looping around all the
372 * directories until none have been successfully removed. This is a
373 * O(N**2) algorithm, so don't try to remove too many nested directories
374 * at once (e.g. thousands).
376 * Note that this will succeed only if each parent directory is writable.
378 while (removed_dirs) {
379 removed_dirs = 0;
380 msg(MSG_DEBUG, "\nAttempting to delete empty dirs\n");
381 for (cur = &extradirs; *cur;) {
382 msg(MSG_QUIET, "%s:\tremoving...", (*cur)->path);
383 if (rmdir((*cur)->path)) {
384 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
385 cur = &(*cur)->list;
386 continue;
388 /* No freeing, because OS will do the job at the end. */
389 *cur = (*cur)->list;
390 removed_dirs++;
391 msg(MSG_QUIET, "ok\n");
396 /* Prints usage message and exits */
397 static void
398 usage(const char *arg0, const char *message)
400 if (message)
401 msg(MSG_CRITICAL, "%s: %s\n\n", arg0, message);
402 msg(MSG_CRITICAL,
403 "Usage: %s ACTION [OPTION...] [PATH...]\n",
404 arg0);
405 msg(MSG_CRITICAL,
406 "\n"
407 "Where ACTION is one of:\n"
408 " -c, --compare Show differences between stored and real metadata\n"
409 " -s, --save Save current metadata\n"
410 " -a, --apply Apply stored metadata\n"
411 " -h, --help Help message (this text)\n"
412 "\n"
413 "Valid OPTIONS are:\n"
414 " -v, --verbose Print more verbose messages\n"
415 " -q, --quiet Print less verbose messages\n"
416 " -m, --mtime Also take mtime into account for diff or apply\n"
417 " -e, --empty-dirs Recreate missing empty directories\n"
418 " -E, --remove-empty-dirs Remove extra empty directories\n"
419 " -g, --git Do not omit .git directories\n"
420 " -f, --file=FILE Set metadata file to FILE\n"
423 exit(message ? EXIT_FAILURE : EXIT_SUCCESS);
426 /* Options */
427 static struct option long_options[] = {
428 {"compare", 0, 0, 0},
429 {"save", 0, 0, 0},
430 {"apply", 0, 0, 0},
431 {"help", 0, 0, 0},
432 {"verbose", 0, 0, 0},
433 {"quiet", 0, 0, 0},
434 {"mtime", 0, 0, 0},
435 {"empty-dirs", 0, 0, 0},
436 {"remove-empty-dirs", 0, 0, 0},
437 {"git", 0, 0, 0},
438 {"file", required_argument, 0, 0},
439 {0, 0, 0, 0}
442 /* Main function */
444 main(int argc, char **argv, char **envp)
446 int i, c;
447 struct metahash *real = NULL;
448 struct metahash *stored = NULL;
449 int action = 0;
451 /* Parse options */
452 i = 0;
453 while (1) {
454 int option_index = 0;
455 c = getopt_long(argc, argv, "csahvqmeEgf:",
456 long_options, &option_index);
457 if (c == -1)
458 break;
459 switch (c) {
460 case 0:
461 if (!strcmp("verbose",
462 long_options[option_index].name)) {
463 adjust_verbosity(1);
464 } else if (!strcmp("quiet",
465 long_options[option_index].name)) {
466 adjust_verbosity(-1);
467 } else if (!strcmp("mtime",
468 long_options[option_index].name)) {
469 settings.do_mtime = true;
470 } else if (!strcmp("empty-dirs",
471 long_options[option_index].name)) {
472 settings.do_emptydirs = true;
473 } else if (!strcmp("remove-empty-dirs",
474 long_options[option_index].name)) {
475 settings.do_removeemptydirs = true;
476 } else if (!strcmp("git",
477 long_options[option_index].name)) {
478 settings.do_git = true;
479 } else if (!strcmp("file",
480 long_options[option_index].name)) {
481 settings.metafile = optarg;
482 } else {
483 action |= (1 << option_index);
484 i++;
486 break;
487 case 'c':
488 action |= ACTION_DIFF;
489 i++;
490 break;
491 case 's':
492 action |= ACTION_SAVE;
493 i++;
494 break;
495 case 'a':
496 action |= ACTION_APPLY;
497 i++;
498 break;
499 case 'h':
500 action |= ACTION_HELP;
501 i++;
502 break;
503 case 'v':
504 adjust_verbosity(1);
505 break;
506 case 'q':
507 adjust_verbosity(-1);
508 break;
509 case 'm':
510 settings.do_mtime = true;
511 break;
512 case 'e':
513 settings.do_emptydirs = true;
514 break;
515 case 'E':
516 settings.do_removeemptydirs = true;
517 break;
518 case 'g':
519 settings.do_git = true;
520 break;
521 case 'f':
522 settings.metafile = optarg;
523 break;
524 default:
525 usage(argv[0], "unknown option");
529 /* Make sure only one action is specified */
530 if (i != 1)
531 usage(argv[0], "incorrect option(s)");
533 /* Make sure --empty-dirs is only used with apply */
534 if (settings.do_emptydirs && action != ACTION_APPLY)
535 usage(argv[0], "--empty-dirs is only valid with --apply");
537 /* Make sure --remove-empty-dirs is only used with apply */
538 if (settings.do_removeemptydirs && action != ACTION_APPLY)
539 usage(argv[0], "--remove-empty-dirs is only valid with --apply");
541 /* Perform action */
542 switch (action) {
543 case ACTION_DIFF:
544 mentries_fromfile(&stored, settings.metafile);
545 if (!stored) {
546 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
547 settings.metafile);
548 exit(EXIT_FAILURE);
551 if (optind < argc) {
552 while (optind < argc)
553 mentries_recurse_path(argv[optind++], &real, &settings);
554 } else {
555 mentries_recurse_path(".", &real, &settings);
558 if (!real) {
559 msg(MSG_CRITICAL,
560 "Failed to load metadata from file system\n");
561 exit(EXIT_FAILURE);
564 mentries_compare(real, stored, compare_print, &settings);
565 break;
567 case ACTION_SAVE:
568 if (optind < argc) {
569 while (optind < argc)
570 mentries_recurse_path(argv[optind++], &real, &settings);
571 } else {
572 mentries_recurse_path(".", &real, &settings);
575 if (!real) {
576 msg(MSG_CRITICAL,
577 "Failed to load metadata from file system\n");
578 exit(EXIT_FAILURE);
581 mentries_tofile(real, settings.metafile);
582 break;
584 case ACTION_APPLY:
585 mentries_fromfile(&stored, settings.metafile);
586 if (!stored) {
587 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
588 settings.metafile);
589 exit(EXIT_FAILURE);
592 if (optind < argc) {
593 while (optind < argc)
594 mentries_recurse_path(argv[optind++], &real, &settings);
595 } else {
596 mentries_recurse_path(".", &real, &settings);
599 if (!real) {
600 msg(MSG_CRITICAL,
601 "Failed to load metadata from file system\n");
602 exit(EXIT_FAILURE);
605 mentries_compare(real, stored, compare_fix, &settings);
607 if (settings.do_emptydirs)
608 fixup_emptydirs(real, stored);
609 if (settings.do_removeemptydirs)
610 fixup_newemptydirs();
611 break;
613 case ACTION_HELP:
614 usage(argv[0], NULL);
617 exit(EXIT_SUCCESS);