Add support for deleting directories with -a -E.
[metastore.git] / metastore.c
blob257b00333cf16719d9f053532cdf72bc3d842fc2
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 <attr/xattr.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <unistd.h>
31 #include "metastore.h"
32 #include "settings.h"
33 #include "utils.h"
34 #include "metaentry.h"
36 /* metastore settings */
37 static struct metasettings settings = {
38 .metafile = METAFILE,
39 .do_mtime = false,
40 .do_emptydirs = false,
41 .do_removeemptydirs = false,
42 .do_git = false,
45 /* Used to create lists of dirs / other files which are missing in the fs */
46 static struct metaentry *missingdirs = NULL;
47 static struct metaentry *missingothers = NULL;
49 /* Used to create lists of dirs / other files which are missing in metadata */
50 static struct metaentry *extradirs = NULL;
53 * Inserts an entry in a linked list ordered by pathlen
55 static void
56 insert_entry_plist(struct metaentry **list, struct metaentry *entry)
58 struct metaentry **parent;
60 for (parent = list; *parent; parent = &((*parent)->list)) {
61 if ((*parent)->pathlen > entry->pathlen)
62 break;
65 entry->list = *parent;
66 *parent = entry;
70 * Prints differences between real and stored actual metadata
71 * - for use in mentries_compare
73 static void
74 compare_print(struct metaentry *real, struct metaentry *stored, int cmp)
76 if (!real && !stored) {
77 msg(MSG_ERROR, "%s called with incorrect arguments\n", __FUNCTION__);
78 return;
81 if (cmp == DIFF_NONE) {
82 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
83 return;
86 msg(MSG_QUIET, "%s:\t", real ? real->path : stored->path);
88 if (cmp & DIFF_ADDED)
89 msg(MSG_QUIET, "added ", real->path);
90 if (cmp & DIFF_DELE)
91 msg(MSG_QUIET, "removed ", stored->path);
92 if (cmp & DIFF_OWNER)
93 msg(MSG_QUIET, "owner ");
94 if (cmp & DIFF_GROUP)
95 msg(MSG_QUIET, "group ");
96 if (cmp & DIFF_MODE)
97 msg(MSG_QUIET, "mode ");
98 if (cmp & DIFF_TYPE)
99 msg(MSG_QUIET, "type ");
100 if (cmp & DIFF_MTIME)
101 msg(MSG_QUIET, "mtime ");
102 if (cmp & DIFF_XATTR)
103 msg(MSG_QUIET, "xattr ");
104 msg(MSG_QUIET, "\n");
108 * Tries to change the real metadata to match the stored one
109 * - for use in mentries_compare
111 static void
112 compare_fix(struct metaentry *real, struct metaentry *stored, int cmp)
114 struct group *group;
115 struct passwd *owner;
116 gid_t gid = -1;
117 uid_t uid = -1;
118 struct utimbuf tbuf;
119 unsigned i;
121 if (!real && !stored) {
122 msg(MSG_ERROR, "%s called with incorrect arguments\n",
123 __FUNCTION__);
124 return;
127 if (!real) {
128 if (S_ISDIR(stored->mode))
129 insert_entry_plist(&missingdirs, stored);
130 else
131 insert_entry_plist(&missingothers, stored);
133 msg(MSG_NORMAL, "%s:\tremoved\n", stored->path);
134 return;
137 if (!stored) {
138 if (S_ISDIR(real->mode))
139 insert_entry_plist(&extradirs, real);
140 msg(MSG_NORMAL, "%s:\tadded\n", real->path);
141 return;
144 if (cmp == DIFF_NONE) {
145 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
146 return;
149 if (cmp & DIFF_TYPE) {
150 msg(MSG_NORMAL, "%s:\tnew type, will not change metadata\n",
151 real->path);
152 return;
155 msg(MSG_QUIET, "%s:\tchanging metadata\n", real->path);
157 while (cmp & (DIFF_OWNER | DIFF_GROUP)) {
158 if (cmp & DIFF_OWNER) {
159 msg(MSG_NORMAL, "%s:\tchanging owner from %s to %s\n",
160 real->path, real->group, stored->group);
161 owner = xgetpwnam(stored->owner);
162 if (!owner) {
163 msg(MSG_DEBUG, "\tgetpwnam failed: %s\n",
164 strerror(errno));
165 break;
167 uid = owner->pw_uid;
170 if (cmp & DIFF_GROUP) {
171 msg(MSG_NORMAL, "%s:\tchanging group from %s to %s\n",
172 real->path, real->group, stored->group);
173 group = xgetgrnam(stored->group);
174 if (!group) {
175 msg(MSG_DEBUG, "\tgetgrnam failed: %s\n",
176 strerror(errno));
177 break;
179 gid = group->gr_gid;
182 if (lchown(real->path, uid, gid)) {
183 msg(MSG_DEBUG, "\tlchown failed: %s\n",
184 strerror(errno));
185 break;
187 break;
190 if (cmp & DIFF_MODE) {
191 msg(MSG_NORMAL, "%s:\tchanging mode from 0%o to 0%o\n",
192 real->path, real->mode & 07777, stored->mode & 07777);
193 if (chmod(real->path, stored->mode & 07777))
194 msg(MSG_DEBUG, "\tchmod failed: %s\n", strerror(errno));
197 /* FIXME: Use utimensat here, or even better - lutimensat */
198 if ((cmp & DIFF_MTIME) && S_ISLNK(real->mode)) {
199 msg(MSG_NORMAL, "%s:\tsymlink, not changing mtime\n", real->path);
200 } else if (cmp & DIFF_MTIME) {
201 msg(MSG_NORMAL, "%s:\tchanging mtime from %ld to %ld\n",
202 real->path, real->mtime, stored->mtime);
203 tbuf.actime = stored->mtime;
204 tbuf.modtime = stored->mtime;
205 if (utime(real->path, &tbuf)) {
206 msg(MSG_DEBUG, "\tutime failed: %s\n", strerror(errno));
207 return;
211 if (cmp & DIFF_XATTR) {
212 for (i = 0; i < real->xattrs; i++) {
213 /* Any attrs to remove? */
214 if (mentry_find_xattr(stored, real, i) >= 0)
215 continue;
217 msg(MSG_NORMAL, "%s:\tremoving xattr %s\n",
218 real->path, real->xattr_names[i]);
219 if (lremovexattr(real->path, real->xattr_names[i]))
220 msg(MSG_DEBUG, "\tlremovexattr failed: %s\n",
221 strerror(errno));
224 for (i = 0; i < stored->xattrs; i++) {
225 /* Any xattrs to add? (on change they are removed above) */
226 if (mentry_find_xattr(real, stored, i) >= 0)
227 continue;
229 msg(MSG_NORMAL, "%s:\tadding xattr %s\n",
230 stored->path, stored->xattr_names[i]);
231 if (lsetxattr(stored->path, stored->xattr_names[i],
232 stored->xattr_values[i],
233 stored->xattr_lvalues[i], XATTR_CREATE))
234 msg(MSG_DEBUG, "\tlsetxattr failed: %s\n",
235 strerror(errno));
241 * Tries to fix any empty dirs which are missing from the filesystem by
242 * recreating them.
244 static void
245 fixup_emptydirs(struct metahash *real, struct metahash *stored)
247 struct metaentry *entry;
248 struct metaentry *cur;
249 struct metaentry **parent;
250 char *bpath;
251 char *delim;
252 size_t blen;
253 struct metaentry *new;
255 if (!missingdirs)
256 return;
257 msg(MSG_DEBUG, "\nAttempting to recreate missing dirs\n");
259 /* If directory x/y is missing, but file x/y/z is also missing,
260 * we should prune directory x/y from the list of directories to
261 * recreate since the deletition of x/y is likely to be genuine
262 * (as opposed to empty dir pruning like git/cvs does).
264 * Also, if file x/y/z is missing, any child directories of
265 * x/y should be pruned as they are probably also intentionally
266 * removed.
269 msg(MSG_DEBUG, "List of candidate dirs:\n");
270 for (cur = missingdirs; cur; cur = cur->list)
271 msg(MSG_DEBUG, " %s\n", cur->path);
273 for (entry = missingothers; entry; entry = entry->list) {
274 msg(MSG_DEBUG, "Pruning using file %s\n", entry->path);
275 bpath = xstrdup(entry->path);
276 delim = strrchr(bpath, '/');
277 if (!delim) {
278 msg(MSG_NORMAL, "No delimiter found in %s\n", bpath);
279 free(bpath);
280 continue;
282 *delim = '\0';
284 parent = &missingdirs;
285 for (cur = *parent; cur; cur = cur->list) {
286 if (strcmp(cur->path, bpath)) {
287 parent = &cur->list;
288 continue;
291 msg(MSG_DEBUG, "Prune phase 1 - %s\n", cur->path);
292 *parent = cur->list;
295 /* Now also prune subdirs of the base dir */
296 *delim++ = '/';
297 *delim = '\0';
298 blen = strlen(bpath);
300 parent = &missingdirs;
301 for (cur = *parent; cur; cur = cur->list) {
302 if (strncmp(cur->path, bpath, blen)) {
303 parent = &cur->list;
304 continue;
307 msg(MSG_DEBUG, "Prune phase 2 - %s\n", cur->path);
308 *parent = cur->list;
311 free(bpath);
313 msg(MSG_DEBUG, "\n");
315 for (cur = missingdirs; cur; cur = cur->list) {
316 msg(MSG_QUIET, "%s:\trecreating...", cur->path);
317 if (mkdir(cur->path, cur->mode)) {
318 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
319 continue;
321 msg(MSG_QUIET, "ok\n");
323 new = mentry_create(cur->path);
324 if (!new) {
325 msg(MSG_QUIET, "Failed to get metadata for %s\n");
326 continue;
329 compare_fix(new, cur, mentry_compare(new, cur, &settings));
334 * Deletes any empty dirs present in the filesystem that are missing
335 * from the metadata.
336 * An "empty" dir is one which either:
337 * - is empty; or
338 * - only contains empty dirs
340 static void
341 fixup_newemptydirs(void)
343 struct metaentry *cur;
344 int removed_dirs = 1;
346 if (!extradirs)
347 return;
349 /* This is a simpleminded algorithm that attempts to rmdir() all
350 * directories discovered missing from the metadata. Naturally, this will
351 * succeed only on the truly empty directories, but depending on the order,
352 * it may mean that parent directory removal are attempted to be removed
353 * *before* the children. To circumvent this, keep looping around all the
354 * directories until none have been successfully removed. This is a
355 * O(N**2) algorithm, so don't try to remove too many nested directories
356 * at once (e.g. thousands).
358 * Note that this will succeed only if each parent directory is writable.
360 while (removed_dirs) {
361 removed_dirs = 0;
362 msg(MSG_DEBUG, "\nAttempting to delete empty dirs\n");
363 for (cur = extradirs; cur; cur = cur->list) {
364 msg(MSG_QUIET, "%s:\tremoving...", cur->path);
365 if (rmdir(cur->path)) {
366 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
367 continue;
369 removed_dirs++;
370 msg(MSG_QUIET, "ok\n");
375 /* Prints usage message and exits */
376 static void
377 usage(const char *arg0, const char *message)
379 if (message)
380 msg(MSG_CRITICAL, "%s: %s\n\n", arg0, message);
381 msg(MSG_CRITICAL, "Usage: %s ACTION [OPTION...] [PATH...]\n\n", arg0);
382 msg(MSG_CRITICAL, "Where ACTION is one of:\n"
383 " -c, --compare\t\tShow differences between stored and real metadata\n"
384 " -s, --save\t\tSave current metadata\n"
385 " -a, --apply\t\tApply stored metadata\n"
386 " -h, --help\t\tHelp message (this text)\n\n"
387 "Valid OPTIONS are:\n"
388 " -v, --verbose\t\tPrint more verbose messages\n"
389 " -q, --quiet\t\tPrint less verbose messages\n"
390 " -m, --mtime\t\tAlso take mtime into account for diff or apply\n"
391 " -E\t\t\tRemove extra empty directories\n"
392 " -e, --empty-dirs\tRecreate missing empty directories\n"
393 " -g, --git\t\tDo not omit .git directories\n"
394 " -f, --file <file>\tSet metadata file\n"
397 exit(message ? EXIT_FAILURE : EXIT_SUCCESS);
400 /* Options */
401 static struct option long_options[] = {
402 {"compare", 0, 0, 0},
403 {"save", 0, 0, 0},
404 {"apply", 0, 0, 0},
405 {"help", 0, 0, 0},
406 {"verbose", 0, 0, 0},
407 {"quiet", 0, 0, 0},
408 {"mtime", 0, 0, 0},
409 {"empty-dirs", 0, 0, 0},
410 {"remove-empty-dirs", 0, 0, 0},
411 {"git", 0, 0, 0},
412 {"file", required_argument, 0, 0},
413 {0, 0, 0, 0}
416 /* Main function */
418 main(int argc, char **argv, char **envp)
420 int i, c;
421 struct metahash *real = NULL;
422 struct metahash *stored = NULL;
423 int action = 0;
425 /* Parse options */
426 i = 0;
427 while (1) {
428 int option_index = 0;
429 c = getopt_long(argc, argv, "csahvqmeEgf:",
430 long_options, &option_index);
431 if (c == -1)
432 break;
433 switch (c) {
434 case 0:
435 if (!strcmp("verbose",
436 long_options[option_index].name)) {
437 adjust_verbosity(1);
438 } else if (!strcmp("quiet",
439 long_options[option_index].name)) {
440 adjust_verbosity(-1);
441 } else if (!strcmp("mtime",
442 long_options[option_index].name)) {
443 settings.do_mtime = true;
444 } else if (!strcmp("empty-dirs",
445 long_options[option_index].name)) {
446 settings.do_emptydirs = true;
447 } else if (!strcmp("remove-empty-dirs",
448 long_options[option_index].name)) {
449 settings.do_removeemptydirs = true;
450 } else if (!strcmp("git",
451 long_options[option_index].name)) {
452 settings.do_git = true;
453 } else if (!strcmp("file",
454 long_options[option_index].name)) {
455 settings.metafile = optarg;
456 } else {
457 action |= (1 << option_index);
458 i++;
460 break;
461 case 'c':
462 action |= ACTION_DIFF;
463 i++;
464 break;
465 case 's':
466 action |= ACTION_SAVE;
467 i++;
468 break;
469 case 'a':
470 action |= ACTION_APPLY;
471 i++;
472 break;
473 case 'h':
474 action |= ACTION_HELP;
475 i++;
476 break;
477 case 'v':
478 adjust_verbosity(1);
479 break;
480 case 'q':
481 adjust_verbosity(-1);
482 break;
483 case 'm':
484 settings.do_mtime = true;
485 break;
486 case 'e':
487 settings.do_emptydirs = true;
488 break;
489 case 'E':
490 settings.do_removeemptydirs = true;
491 break;
492 case 'g':
493 settings.do_git = true;
494 break;
495 case 'f':
496 settings.metafile = optarg;
497 break;
498 default:
499 usage(argv[0], "unknown option");
503 /* Make sure only one action is specified */
504 if (i != 1)
505 usage(argv[0], "incorrect option(s)");
507 /* Make sure --empty-dirs is only used with apply */
508 if (settings.do_emptydirs && action != ACTION_APPLY)
509 usage(argv[0], "--empty-dirs is only valid with --apply");
511 /* Make sure --remove-empty-dirs is only used with apply */
512 if (settings.do_removeemptydirs && action != ACTION_APPLY)
513 usage(argv[0], "--remove-empty-dirs is only valid with --apply");
515 /* Perform action */
516 switch (action) {
517 case ACTION_DIFF:
518 mentries_fromfile(&stored, settings.metafile);
519 if (!stored) {
520 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
521 settings.metafile);
522 exit(EXIT_FAILURE);
525 if (optind < argc) {
526 while (optind < argc)
527 mentries_recurse_path(argv[optind++], &real, &settings);
528 } else {
529 mentries_recurse_path(".", &real, &settings);
532 if (!real) {
533 msg(MSG_CRITICAL,
534 "Failed to load metadata from file system\n");
535 exit(EXIT_FAILURE);
538 mentries_compare(real, stored, compare_print, &settings);
539 break;
541 case ACTION_SAVE:
542 if (optind < argc) {
543 while (optind < argc)
544 mentries_recurse_path(argv[optind++], &real, &settings);
545 } else {
546 mentries_recurse_path(".", &real, &settings);
549 if (!real) {
550 msg(MSG_CRITICAL,
551 "Failed to load metadata from file system\n");
552 exit(EXIT_FAILURE);
555 mentries_tofile(real, settings.metafile);
556 break;
558 case ACTION_APPLY:
559 mentries_fromfile(&stored, settings.metafile);
560 if (!stored) {
561 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
562 settings.metafile);
563 exit(EXIT_FAILURE);
566 if (optind < argc) {
567 while (optind < argc)
568 mentries_recurse_path(argv[optind++], &real, &settings);
569 } else {
570 mentries_recurse_path(".", &real, &settings);
573 if (!real) {
574 msg(MSG_CRITICAL,
575 "Failed to load metadata from file system\n");
576 exit(EXIT_FAILURE);
579 mentries_compare(real, stored, compare_fix, &settings);
581 if (settings.do_emptydirs)
582 fixup_emptydirs(real, stored);
583 if (settings.do_removeemptydirs)
584 fixup_newemptydirs();
585 break;
587 case ACTION_HELP:
588 usage(argv[0], NULL);
591 exit(EXIT_SUCCESS);