Support building with no extended attributes support.
[metastore.git] / src / metastore.c
blob9f01a8c1bbfc1eb86d6e306079145eeee4f6f385
1 /* SPDX-License-Identifier: GPL-2.0-only */
2 /*
3 * Main functions of the program.
5 * Copyright (C) 2007-2008 David Härdeman <david@hardeman.nu>
6 * Copyright (C) 2012-2016 Przemyslaw Pawelczyk <przemoc@gmail.com>
7 * Copyright (C) 2014-2015 Dan Fandrich <dan@coneharvesters.com>
9 * This program is free software; you can redistribute it and/or modify it
10 * under the terms of the GNU General Public License as published by the
11 * Free Software Foundation; only version 2 of the License is applicable.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 * See the GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #define _BSD_SOURCE
23 #define _DEFAULT_SOURCE
24 #include <sys/types.h>
25 #include <sys/stat.h>
26 #include <getopt.h>
27 #include <utime.h>
29 #if !defined(NO_XATTR) || !NO_XATTR
30 # include <sys/xattr.h>
31 #endif /* !NO_XATTR */
33 #include <stdlib.h>
34 #include <string.h>
35 #include <unistd.h>
36 #include <errno.h>
38 #include "metastore.h"
39 #include "settings.h"
40 #include "utils.h"
41 #include "metaentry.h"
43 /* metastore settings */
44 static struct metasettings settings = {
45 .metafile = METAFILE,
46 .do_mtime = false,
47 .do_emptydirs = false,
48 .do_removeemptydirs = false,
49 .do_git = false,
52 /* Used to create lists of dirs / other files which are missing in the fs */
53 static struct metaentry *missingdirs = NULL;
54 static struct metaentry *missingothers = NULL;
56 /* Used to create lists of dirs / other files which are missing in metadata */
57 static struct metaentry *extradirs = NULL;
60 * Inserts an entry in a linked list ordered by pathlen
62 static void
63 insert_entry_plist(struct metaentry **list, struct metaentry *entry)
65 struct metaentry **parent;
67 for (parent = list; *parent; parent = &((*parent)->list)) {
68 if ((*parent)->pathlen > entry->pathlen)
69 break;
72 entry->list = *parent;
73 *parent = entry;
77 * Inserts an entry in a linked list ordered by pathlen descendingly
79 static void
80 insert_entry_pdlist(struct metaentry **list, struct metaentry *entry)
82 struct metaentry **parent;
84 for (parent = list; *parent; parent = &((*parent)->list)) {
85 if ((*parent)->pathlen < entry->pathlen)
86 break;
89 entry->list = *parent;
90 *parent = entry;
94 * Prints differences between real and stored actual metadata
95 * - for use in mentries_compare
97 static void
98 compare_print(struct metaentry *real, struct metaentry *stored, int cmp)
100 if (!real && (!stored || (cmp == DIFF_NONE || cmp & DIFF_ADDED))) {
101 msg(MSG_ERROR, "%s called with incorrect arguments\n", __func__);
102 return;
105 if (cmp == DIFF_NONE) {
106 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
107 return;
110 msg(MSG_QUIET, "%s:\t", real ? real->path : stored->path);
112 if (cmp & DIFF_ADDED)
113 msg(MSG_QUIET, "added ", real->path);
114 if (cmp & DIFF_DELE)
115 msg(MSG_QUIET, "removed ", stored->path);
116 if (cmp & DIFF_OWNER)
117 msg(MSG_QUIET, "owner ");
118 if (cmp & DIFF_GROUP)
119 msg(MSG_QUIET, "group ");
120 if (cmp & DIFF_MODE)
121 msg(MSG_QUIET, "mode ");
122 if (cmp & DIFF_TYPE)
123 msg(MSG_QUIET, "type ");
124 if (cmp & DIFF_MTIME)
125 msg(MSG_QUIET, "mtime ");
126 if (cmp & DIFF_XATTR)
127 msg(MSG_QUIET, "xattr ");
128 msg(MSG_QUIET, "\n");
130 if (NO_XATTR && cmp & DIFF_XATTR) {
131 msg(MSG_WARNING, "%s:\txattr difference may be bogus: %s\n",
132 real->path, NO_XATTR_MSG);
137 * Tries to change the real metadata to match the stored one
138 * - for use in mentries_compare
140 static void
141 compare_fix(struct metaentry *real, struct metaentry *stored, int cmp)
143 struct group *group;
144 struct passwd *owner;
145 gid_t gid = -1;
146 uid_t uid = -1;
147 struct utimbuf tbuf;
148 unsigned i;
150 if (!real && !stored) {
151 msg(MSG_ERROR, "%s called with incorrect arguments\n", __func__);
152 return;
155 if (!real) {
156 if (S_ISDIR(stored->mode))
157 insert_entry_plist(&missingdirs, stored);
158 else
159 insert_entry_plist(&missingothers, stored);
161 msg(MSG_NORMAL, "%s:\tremoved\n", stored->path);
162 return;
165 if (!stored) {
166 if (S_ISDIR(real->mode))
167 insert_entry_pdlist(&extradirs, real);
168 msg(MSG_NORMAL, "%s:\tadded\n", real->path);
169 return;
172 if (cmp == DIFF_NONE) {
173 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
174 return;
177 if (cmp & DIFF_TYPE) {
178 msg(MSG_NORMAL, "%s:\tnew type, will not change metadata\n",
179 real->path);
180 return;
183 msg(MSG_QUIET, "%s:\tchanging metadata\n", real->path);
185 while (cmp & (DIFF_OWNER | DIFF_GROUP)) {
186 if (cmp & DIFF_OWNER) {
187 msg(MSG_NORMAL, "%s:\tchanging owner from %s to %s\n",
188 real->path, real->group, stored->group);
189 owner = xgetpwnam(stored->owner);
190 if (!owner) {
191 msg(MSG_DEBUG, "\tgetpwnam failed: %s\n",
192 strerror(errno));
193 break;
195 uid = owner->pw_uid;
198 if (cmp & DIFF_GROUP) {
199 msg(MSG_NORMAL, "%s:\tchanging group from %s to %s\n",
200 real->path, real->group, stored->group);
201 group = xgetgrnam(stored->group);
202 if (!group) {
203 msg(MSG_DEBUG, "\tgetgrnam failed: %s\n",
204 strerror(errno));
205 break;
207 gid = group->gr_gid;
210 if (lchown(real->path, uid, gid)) {
211 msg(MSG_DEBUG, "\tlchown failed: %s\n",
212 strerror(errno));
213 break;
215 break;
218 if (cmp & DIFF_MODE) {
219 msg(MSG_NORMAL, "%s:\tchanging mode from 0%o to 0%o\n",
220 real->path, real->mode & 07777, stored->mode & 07777);
221 if (chmod(real->path, stored->mode & 07777))
222 msg(MSG_DEBUG, "\tchmod failed: %s\n", strerror(errno));
225 /* FIXME: Use utimensat here, or even better - lutimensat */
226 if ((cmp & DIFF_MTIME) && S_ISLNK(real->mode)) {
227 msg(MSG_NORMAL, "%s:\tsymlink, not changing mtime\n", real->path);
228 } else if (cmp & DIFF_MTIME) {
229 msg(MSG_NORMAL, "%s:\tchanging mtime from %ld to %ld\n",
230 real->path, real->mtime, stored->mtime);
231 tbuf.actime = stored->mtime;
232 tbuf.modtime = stored->mtime;
233 if (utime(real->path, &tbuf)) {
234 msg(MSG_DEBUG, "\tutime failed: %s\n", strerror(errno));
235 return;
239 if (cmp & DIFF_XATTR) {
240 for (i = 0; i < real->xattrs; i++) {
241 /* Any attrs to remove? */
242 if (mentry_find_xattr(stored, real, i) >= 0)
243 continue;
245 msg(MSG_NORMAL, "%s:\tremoving xattr %s\n",
246 real->path, real->xattr_names[i]);
247 if (NO_XATTR) {
248 msg(MSG_WARNING, "%s:\tremoving xattr %s failed: %s\n",
249 real->path, real->xattr_names[i], NO_XATTR_MSG);
251 #if !defined(NO_XATTR) || !NO_XATTR
252 else
253 if (lremovexattr(real->path, real->xattr_names[i]))
254 msg(MSG_DEBUG, "\tlremovexattr failed: %s\n",
255 strerror(errno));
256 #endif /* !NO_XATTR */
259 for (i = 0; i < stored->xattrs; i++) {
260 /* Any xattrs to add? (on change they are removed above) */
261 if (mentry_find_xattr(real, stored, i) >= 0)
262 continue;
264 msg(MSG_NORMAL, "%s:\tadding xattr %s\n",
265 stored->path, stored->xattr_names[i]);
266 if (NO_XATTR) {
267 msg(MSG_WARNING, "%s:\tadding xattr %s failed: %s\n",
268 stored->path, stored->xattr_names[i], NO_XATTR_MSG);
270 #if !defined(NO_XATTR) || !NO_XATTR
271 else
272 if (lsetxattr(stored->path, stored->xattr_names[i],
273 stored->xattr_values[i],
274 stored->xattr_lvalues[i], XATTR_CREATE)
276 msg(MSG_DEBUG, "\tlsetxattr failed: %s\n",
277 strerror(errno));
278 #endif /* !NO_XATTR */
284 * Tries to fix any empty dirs which are missing from the filesystem by
285 * recreating them.
287 static void
288 fixup_emptydirs(void)
290 struct metaentry *entry;
291 struct metaentry *cur;
292 struct metaentry **parent;
293 char *bpath;
294 char *delim;
295 size_t blen;
296 struct metaentry *new;
298 if (!missingdirs)
299 return;
300 msg(MSG_DEBUG, "\nAttempting to recreate missing dirs\n");
302 /* If directory x/y is missing, but file x/y/z is also missing,
303 * we should prune directory x/y from the list of directories to
304 * recreate since the deletition of x/y is likely to be genuine
305 * (as opposed to empty dir pruning like git/cvs does).
307 * Also, if file x/y/z is missing, any child directories of
308 * x/y should be pruned as they are probably also intentionally
309 * removed.
312 msg(MSG_DEBUG, "List of candidate dirs:\n");
313 for (cur = missingdirs; cur; cur = cur->list)
314 msg(MSG_DEBUG, " %s\n", cur->path);
316 for (entry = missingothers; entry; entry = entry->list) {
317 msg(MSG_DEBUG, "Pruning using file %s\n", entry->path);
318 bpath = xstrdup(entry->path);
319 delim = strrchr(bpath, '/');
320 if (!delim) {
321 msg(MSG_NORMAL, "No delimiter found in %s\n", bpath);
322 free(bpath);
323 continue;
325 *delim = '\0';
327 parent = &missingdirs;
328 for (cur = *parent; cur; cur = cur->list) {
329 if (strcmp(cur->path, bpath)) {
330 parent = &cur->list;
331 continue;
334 msg(MSG_DEBUG, "Prune phase 1 - %s\n", cur->path);
335 *parent = cur->list;
338 /* Now also prune subdirs of the base dir */
339 *delim++ = '/';
340 *delim = '\0';
341 blen = strlen(bpath);
343 parent = &missingdirs;
344 for (cur = *parent; cur; cur = cur->list) {
345 if (strncmp(cur->path, bpath, blen)) {
346 parent = &cur->list;
347 continue;
350 msg(MSG_DEBUG, "Prune phase 2 - %s\n", cur->path);
351 *parent = cur->list;
354 free(bpath);
356 msg(MSG_DEBUG, "\n");
358 for (cur = missingdirs; cur; cur = cur->list) {
359 msg(MSG_QUIET, "%s:\trecreating...", cur->path);
360 if (mkdir(cur->path, cur->mode)) {
361 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
362 continue;
364 msg(MSG_QUIET, "ok\n");
366 new = mentry_create(cur->path);
367 if (!new) {
368 msg(MSG_QUIET, "Failed to get metadata for %s\n");
369 continue;
372 compare_fix(new, cur, mentry_compare(new, cur, &settings));
377 * Deletes any empty dirs present in the filesystem that are missing
378 * from the metadata.
379 * An "empty" dir is one which either:
380 * - is empty; or
381 * - only contains empty dirs
383 static void
384 fixup_newemptydirs(void)
386 struct metaentry **cur;
387 int removed_dirs = 1;
389 if (!extradirs)
390 return;
392 /* This is a simpleminded algorithm that attempts to rmdir() all
393 * directories discovered missing from the metadata. Naturally, this will
394 * succeed only on the truly empty directories, but depending on the order,
395 * it may mean that parent directory removal are attempted to be removed
396 * *before* the children. To circumvent this, keep looping around all the
397 * directories until none have been successfully removed. This is a
398 * O(N**2) algorithm, so don't try to remove too many nested directories
399 * at once (e.g. thousands).
401 * Note that this will succeed only if each parent directory is writable.
403 while (removed_dirs) {
404 removed_dirs = 0;
405 msg(MSG_DEBUG, "\nAttempting to delete empty dirs\n");
406 for (cur = &extradirs; *cur;) {
407 msg(MSG_QUIET, "%s:\tremoving...", (*cur)->path);
408 if (rmdir((*cur)->path)) {
409 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
410 cur = &(*cur)->list;
411 continue;
413 /* No freeing, because OS will do the job at the end. */
414 *cur = (*cur)->list;
415 removed_dirs++;
416 msg(MSG_QUIET, "ok\n");
421 /* Outputs version information and exits */
422 static void
423 version(void)
425 printf("metastore %s\n", METASTORE_VER);
427 if (NO_XATTR) {
428 printf("Built with %s.\n", NO_XATTR_MSG);
431 exit(EXIT_SUCCESS);
434 /* Prints usage message and exits */
435 static void
436 usage(const char *arg0, const char *message)
438 if (message) {
439 msg(MSG_CRITICAL, "%s: %s\n", arg0, message);
440 msg(MSG_ERROR, "\n");
443 msg(message ? MSG_ERROR : MSG_QUIET,
444 "Usage: %s ACTION [OPTION...] [PATH...]\n",
445 arg0);
446 msg(message ? MSG_ERROR : MSG_QUIET,
447 "\n"
448 "Where ACTION is one of:\n"
449 " -c, --compare Show differences between stored and real metadata\n"
450 " -s, --save Save current metadata\n"
451 " -a, --apply Apply stored metadata\n"
452 " -d, --dump Dump stored (if no PATH is given) or real metadata\n"
453 " (if PATH is present, e.g. ./) in human-readable form\n"
454 " -V, --version Output version information and exit\n"
455 " -h, --help Help message (this text)\n"
456 "\n"
457 "Valid OPTIONS are:\n"
458 " -v, --verbose Print more verbose messages\n"
459 " -q, --quiet Print less verbose messages\n"
460 " -m, --mtime Also take mtime into account for diff or apply\n"
461 " -e, --empty-dirs Recreate missing empty directories\n"
462 " -E, --remove-empty-dirs Remove extra empty directories\n"
463 " -g, --git Do not omit .git directories\n"
464 " -f, --file=FILE Set metadata file (" METAFILE " by default)\n"
467 exit(message ? EXIT_FAILURE : EXIT_SUCCESS);
470 /* Options */
471 static struct option long_options[] = {
472 { "compare", no_argument, NULL, 'c' },
473 { "save", no_argument, NULL, 's' },
474 { "apply", no_argument, NULL, 'a' },
475 { "dump", no_argument, NULL, 'd' },
476 { "version", no_argument, NULL, 'V' },
477 { "help", no_argument, NULL, 'h' },
478 { "verbose", no_argument, NULL, 'v' },
479 { "quiet", no_argument, NULL, 'q' },
480 { "mtime", no_argument, NULL, 'm' },
481 { "empty-dirs", no_argument, NULL, 'e' },
482 { "remove-empty-dirs", no_argument, NULL, 'E' },
483 { "git", no_argument, NULL, 'g' },
484 { "file", required_argument, NULL, 'f' },
485 { NULL, 0, NULL, 0 }
488 /* Main function */
490 main(int argc, char **argv)
492 int i, c;
493 struct metahash *real = NULL;
494 struct metahash *stored = NULL;
495 int action = 0;
497 /* Parse options */
498 i = 0;
499 while (1) {
500 int option_index = 0;
501 c = getopt_long(argc, argv, "csadVhvqmeEgf:",
502 long_options, &option_index);
503 if (c == -1)
504 break;
505 switch (c) {
506 case 'c': /* compare */ action |= ACTION_DIFF; i++; break;
507 case 's': /* save */ action |= ACTION_SAVE; i++; break;
508 case 'a': /* apply */ action |= ACTION_APPLY; i++; break;
509 case 'd': /* dump */ action |= ACTION_DUMP; i++; break;
510 case 'V': /* version */ action |= ACTION_VER; i++; break;
511 case 'h': /* help */ action |= ACTION_HELP; i++; break;
512 case 'v': /* verbose */ adjust_verbosity(1); break;
513 case 'q': /* quiet */ adjust_verbosity(-1); break;
514 case 'm': /* mtime */ settings.do_mtime = true; break;
515 case 'e': /* empty-dirs */ settings.do_emptydirs = true; break;
516 case 'E': /* remove-empty-dirs */ settings.do_removeemptydirs = true;
517 break;
518 case 'g': /* git */ settings.do_git = true; break;
519 case 'f': /* file */ settings.metafile = optarg; break;
520 default:
521 usage(argv[0], "unknown option");
525 /* Make sure only one action is specified */
526 if (i != 1)
527 usage(argv[0], "incorrect option(s)");
529 /* Make sure --empty-dirs is only used with apply */
530 if (settings.do_emptydirs && action != ACTION_APPLY)
531 usage(argv[0], "--empty-dirs is only valid with --apply");
533 /* Make sure --remove-empty-dirs is only used with apply */
534 if (settings.do_removeemptydirs && action != ACTION_APPLY)
535 usage(argv[0], "--remove-empty-dirs is only valid with --apply");
537 if (action == ACTION_VER)
538 version();
540 if (action == ACTION_HELP)
541 usage(argv[0], NULL);
543 /* Perform action */
544 if (action & ACTIONS_READING && !(action == ACTION_DUMP && optind < argc)) {
545 mentries_fromfile(&stored, settings.metafile);
546 if (!stored) {
547 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
548 settings.metafile);
549 exit(EXIT_FAILURE);
553 if (optind < argc) {
554 while (optind < argc)
555 mentries_recurse_path(argv[optind++], &real, &settings);
556 } else if (action != ACTION_DUMP) {
557 mentries_recurse_path(".", &real, &settings);
560 if (!real && (action != ACTION_DUMP || optind < argc)) {
561 msg(MSG_CRITICAL,
562 "Failed to load metadata from file system\n");
563 exit(EXIT_FAILURE);
566 switch (action) {
567 case ACTION_DIFF:
568 mentries_compare(real, stored, compare_print, &settings);
569 break;
570 case ACTION_SAVE:
571 mentries_tofile(real, settings.metafile);
572 break;
573 case ACTION_APPLY:
574 mentries_compare(real, stored, compare_fix, &settings);
575 if (settings.do_emptydirs)
576 fixup_emptydirs();
577 if (settings.do_removeemptydirs)
578 fixup_newemptydirs();
579 break;
580 case ACTION_DUMP:
581 mentries_dump(real ? real : stored);
582 break;
585 exit(EXIT_SUCCESS);