AUTHORS: Add Ken Dawson.
[metastore.git] / metastore.c
blob83f855550e18b5cc4dca41a0a4358d3b5655ccd7
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_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;
50 * Inserts an entry in a linked list ordered by pathlen
52 static void
53 insert_entry_plist(struct metaentry **list, struct metaentry *entry)
55 struct metaentry **parent;
57 for (parent = list; *parent; parent = &((*parent)->list)) {
58 if ((*parent)->pathlen > entry->pathlen)
59 break;
62 entry->list = *parent;
63 *parent = entry;
67 * Prints differences between real and stored actual metadata
68 * - for use in mentries_compare
70 static void
71 compare_print(struct metaentry *real, struct metaentry *stored, int cmp)
73 if (!real && !stored) {
74 msg(MSG_ERROR, "%s called with incorrect arguments\n", __FUNCTION__);
75 return;
78 if (cmp == DIFF_NONE) {
79 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
80 return;
83 msg(MSG_QUIET, "%s:\t", real ? real->path : stored->path);
85 if (cmp & DIFF_ADDED)
86 msg(MSG_QUIET, "added ", real->path);
87 if (cmp & DIFF_DELE)
88 msg(MSG_QUIET, "removed ", stored->path);
89 if (cmp & DIFF_OWNER)
90 msg(MSG_QUIET, "owner ");
91 if (cmp & DIFF_GROUP)
92 msg(MSG_QUIET, "group ");
93 if (cmp & DIFF_MODE)
94 msg(MSG_QUIET, "mode ");
95 if (cmp & DIFF_TYPE)
96 msg(MSG_QUIET, "type ");
97 if (cmp & DIFF_MTIME)
98 msg(MSG_QUIET, "mtime ");
99 if (cmp & DIFF_XATTR)
100 msg(MSG_QUIET, "xattr ");
101 msg(MSG_QUIET, "\n");
105 * Tries to change the real metadata to match the stored one
106 * - for use in mentries_compare
108 static void
109 compare_fix(struct metaentry *real, struct metaentry *stored, int cmp)
111 struct group *group;
112 struct passwd *owner;
113 gid_t gid = -1;
114 uid_t uid = -1;
115 struct utimbuf tbuf;
116 unsigned i;
118 if (!real && !stored) {
119 msg(MSG_ERROR, "%s called with incorrect arguments\n",
120 __FUNCTION__);
121 return;
124 if (!real) {
125 if (S_ISDIR(stored->mode))
126 insert_entry_plist(&missingdirs, stored);
127 else
128 insert_entry_plist(&missingothers, stored);
130 msg(MSG_NORMAL, "%s:\tremoved\n", stored->path);
131 return;
134 if (!stored) {
135 msg(MSG_NORMAL, "%s:\tadded\n", real->path);
136 return;
139 if (cmp == DIFF_NONE) {
140 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
141 return;
144 if (cmp & DIFF_TYPE) {
145 msg(MSG_NORMAL, "%s:\tnew type, will not change metadata\n",
146 real->path);
147 return;
150 msg(MSG_QUIET, "%s:\tchanging metadata\n", real->path);
152 while (cmp & (DIFF_OWNER | DIFF_GROUP)) {
153 if (cmp & DIFF_OWNER) {
154 msg(MSG_NORMAL, "%s:\tchanging owner from %s to %s\n",
155 real->path, real->group, stored->group);
156 owner = xgetpwnam(stored->owner);
157 if (!owner) {
158 msg(MSG_DEBUG, "\tgetpwnam failed: %s\n",
159 strerror(errno));
160 break;
162 uid = owner->pw_uid;
165 if (cmp & DIFF_GROUP) {
166 msg(MSG_NORMAL, "%s:\tchanging group from %s to %s\n",
167 real->path, real->group, stored->group);
168 group = xgetgrnam(stored->group);
169 if (!group) {
170 msg(MSG_DEBUG, "\tgetgrnam failed: %s\n",
171 strerror(errno));
172 break;
174 gid = group->gr_gid;
177 if (lchown(real->path, uid, gid)) {
178 msg(MSG_DEBUG, "\tlchown failed: %s\n",
179 strerror(errno));
180 break;
182 break;
185 if (cmp & DIFF_MODE) {
186 msg(MSG_NORMAL, "%s:\tchanging mode from 0%o to 0%o\n",
187 real->path, real->mode & 07777, stored->mode & 07777);
188 if (chmod(real->path, stored->mode & 07777))
189 msg(MSG_DEBUG, "\tchmod failed: %s\n", strerror(errno));
192 /* FIXME: Use utimensat here, or even better - lutimensat */
193 if ((cmp & DIFF_MTIME) && S_ISLNK(real->mode)) {
194 msg(MSG_NORMAL, "%s:\tsymlink, not changing mtime\n", real->path);
195 } else if (cmp & DIFF_MTIME) {
196 msg(MSG_NORMAL, "%s:\tchanging mtime from %ld to %ld\n",
197 real->path, real->mtime, stored->mtime);
198 tbuf.actime = stored->mtime;
199 tbuf.modtime = stored->mtime;
200 if (utime(real->path, &tbuf)) {
201 msg(MSG_DEBUG, "\tutime failed: %s\n", strerror(errno));
202 return;
206 if (cmp & DIFF_XATTR) {
207 for (i = 0; i < real->xattrs; i++) {
208 /* Any attrs to remove? */
209 if (mentry_find_xattr(stored, real, i) >= 0)
210 continue;
212 msg(MSG_NORMAL, "%s:\tremoving xattr %s\n",
213 real->path, real->xattr_names[i]);
214 if (lremovexattr(real->path, real->xattr_names[i]))
215 msg(MSG_DEBUG, "\tlremovexattr failed: %s\n",
216 strerror(errno));
219 for (i = 0; i < stored->xattrs; i++) {
220 /* Any xattrs to add? (on change they are removed above) */
221 if (mentry_find_xattr(real, stored, i) >= 0)
222 continue;
224 msg(MSG_NORMAL, "%s:\tadding xattr %s\n",
225 stored->path, stored->xattr_names[i]);
226 if (lsetxattr(stored->path, stored->xattr_names[i],
227 stored->xattr_values[i],
228 stored->xattr_lvalues[i], XATTR_CREATE))
229 msg(MSG_DEBUG, "\tlsetxattr failed: %s\n",
230 strerror(errno));
236 * Tries to fix any empty dirs which are missing by recreating them.
237 * An "empty" dir is one which either:
238 * - is empty; or
239 * - only contained empty dirs
241 static void
242 fixup_emptydirs(struct metahash *real, struct metahash *stored)
244 struct metaentry *entry;
245 struct metaentry *cur;
246 struct metaentry **parent;
247 char *bpath;
248 char *delim;
249 size_t blen;
250 struct metaentry *new;
252 if (!missingdirs)
253 return;
254 msg(MSG_DEBUG, "\nAttempting to recreate missing dirs\n");
256 /* If directory x/y is missing, but file x/y/z is also missing,
257 * we should prune directory x/y from the list of directories to
258 * recreate since the deletition of x/y is likely to be genuine
259 * (as opposed to empty dir pruning like git/cvs does).
261 * Also, if file x/y/z is missing, any child directories of
262 * x/y should be pruned as they are probably also intentionally
263 * removed.
266 msg(MSG_DEBUG, "List of candidate dirs:\n");
267 for (cur = missingdirs; cur; cur = cur->list)
268 msg(MSG_DEBUG, " %s\n", cur->path);
270 for (entry = missingothers; entry; entry = entry->list) {
271 msg(MSG_DEBUG, "Pruning using file %s\n", entry->path);
272 bpath = xstrdup(entry->path);
273 delim = strrchr(bpath, '/');
274 if (!delim) {
275 msg(MSG_NORMAL, "No delimiter found in %s\n", bpath);
276 free(bpath);
277 continue;
279 *delim = '\0';
281 parent = &missingdirs;
282 for (cur = *parent; cur; cur = cur->list) {
283 if (strcmp(cur->path, bpath)) {
284 parent = &cur->list;
285 continue;
288 msg(MSG_DEBUG, "Prune phase 1 - %s\n", cur->path);
289 *parent = cur->list;
292 /* Now also prune subdirs of the base dir */
293 *delim++ = '/';
294 *delim = '\0';
295 blen = strlen(bpath);
297 parent = &missingdirs;
298 for (cur = *parent; cur; cur = cur->list) {
299 if (strncmp(cur->path, bpath, blen)) {
300 parent = &cur->list;
301 continue;
304 msg(MSG_DEBUG, "Prune phase 2 - %s\n", cur->path);
305 *parent = cur->list;
308 free(bpath);
310 msg(MSG_DEBUG, "\n");
312 for (cur = missingdirs; cur; cur = cur->list) {
313 msg(MSG_QUIET, "%s:\trecreating...", cur->path);
314 if (mkdir(cur->path, cur->mode)) {
315 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
316 continue;
318 msg(MSG_QUIET, "ok\n");
320 new = mentry_create(cur->path);
321 if (!new) {
322 msg(MSG_QUIET, "Failed to get metadata for %s\n");
323 continue;
326 compare_fix(new, cur, mentry_compare(new, cur, &settings));
330 /* Prints usage message and exits */
331 static void
332 usage(const char *arg0, const char *message)
334 if (message)
335 msg(MSG_CRITICAL, "%s: %s\n\n", arg0, message);
336 msg(MSG_CRITICAL, "Usage: %s ACTION [OPTION...] [PATH...]\n\n", arg0);
337 msg(MSG_CRITICAL, "Where ACTION is one of:\n"
338 " -c, --compare\t\tShow differences between stored and real metadata\n"
339 " -s, --save\t\tSave current metadata\n"
340 " -a, --apply\t\tApply stored metadata\n"
341 " -h, --help\t\tHelp message (this text)\n\n"
342 "Valid OPTIONS are:\n"
343 " -v, --verbose\t\tPrint more verbose messages\n"
344 " -q, --quiet\t\tPrint less verbose messages\n"
345 " -m, --mtime\t\tAlso take mtime into account for diff or apply\n"
346 " -e, --empty-dirs\tRecreate missing empty directories (experimental)\n"
347 " -g, --git\t\tDo not omit .git directories\n"
348 " -f, --file <file>\tSet metadata file\n"
351 exit(message ? EXIT_FAILURE : EXIT_SUCCESS);
354 /* Options */
355 static struct option long_options[] = {
356 {"compare", 0, 0, 0},
357 {"save", 0, 0, 0},
358 {"apply", 0, 0, 0},
359 {"help", 0, 0, 0},
360 {"verbose", 0, 0, 0},
361 {"quiet", 0, 0, 0},
362 {"mtime", 0, 0, 0},
363 {"empty-dirs", 0, 0, 0},
364 {"git", 0, 0, 0},
365 {"file", required_argument, 0, 0},
366 {0, 0, 0, 0}
369 /* Main function */
371 main(int argc, char **argv, char **envp)
373 int i, c;
374 struct metahash *real = NULL;
375 struct metahash *stored = NULL;
376 int action = 0;
378 /* Parse options */
379 i = 0;
380 while (1) {
381 int option_index = 0;
382 c = getopt_long(argc, argv, "csahvqmegf:",
383 long_options, &option_index);
384 if (c == -1)
385 break;
386 switch (c) {
387 case 0:
388 if (!strcmp("verbose",
389 long_options[option_index].name)) {
390 adjust_verbosity(1);
391 } else if (!strcmp("quiet",
392 long_options[option_index].name)) {
393 adjust_verbosity(-1);
394 } else if (!strcmp("mtime",
395 long_options[option_index].name)) {
396 settings.do_mtime = true;
397 } else if (!strcmp("empty-dirs",
398 long_options[option_index].name)) {
399 settings.do_emptydirs = true;
400 } else if (!strcmp("git",
401 long_options[option_index].name)) {
402 settings.do_git = true;
403 } else if (!strcmp("file",
404 long_options[option_index].name)) {
405 settings.metafile = optarg;
406 } else {
407 action |= (1 << option_index);
408 i++;
410 break;
411 case 'c':
412 action |= ACTION_DIFF;
413 i++;
414 break;
415 case 's':
416 action |= ACTION_SAVE;
417 i++;
418 break;
419 case 'a':
420 action |= ACTION_APPLY;
421 i++;
422 break;
423 case 'h':
424 action |= ACTION_HELP;
425 i++;
426 break;
427 case 'v':
428 adjust_verbosity(1);
429 break;
430 case 'q':
431 adjust_verbosity(-1);
432 break;
433 case 'm':
434 settings.do_mtime = true;
435 break;
436 case 'e':
437 settings.do_emptydirs = true;
438 break;
439 case 'g':
440 settings.do_git = true;
441 break;
442 case 'f':
443 settings.metafile = optarg;
444 break;
445 default:
446 usage(argv[0], "unknown option");
450 /* Make sure only one action is specified */
451 if (i != 1)
452 usage(argv[0], "incorrect option(s)");
454 /* Make sure --empty-dirs is only used with apply */
455 if (settings.do_emptydirs && action != ACTION_APPLY)
456 usage(argv[0], "--empty-dirs is only valid with --apply");
458 /* Perform action */
459 switch (action) {
460 case ACTION_DIFF:
461 mentries_fromfile(&stored, settings.metafile);
462 if (!stored) {
463 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
464 settings.metafile);
465 exit(EXIT_FAILURE);
468 if (optind < argc) {
469 while (optind < argc)
470 mentries_recurse_path(argv[optind++], &real, &settings);
471 } else {
472 mentries_recurse_path(".", &real, &settings);
475 if (!real) {
476 msg(MSG_CRITICAL,
477 "Failed to load metadata from file system\n");
478 exit(EXIT_FAILURE);
481 mentries_compare(real, stored, compare_print, &settings);
482 break;
484 case ACTION_SAVE:
485 if (optind < argc) {
486 while (optind < argc)
487 mentries_recurse_path(argv[optind++], &real, &settings);
488 } else {
489 mentries_recurse_path(".", &real, &settings);
492 if (!real) {
493 msg(MSG_CRITICAL,
494 "Failed to load metadata from file system\n");
495 exit(EXIT_FAILURE);
498 mentries_tofile(real, settings.metafile);
499 break;
501 case ACTION_APPLY:
502 mentries_fromfile(&stored, settings.metafile);
503 if (!stored) {
504 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
505 settings.metafile);
506 exit(EXIT_FAILURE);
509 if (optind < argc) {
510 while (optind < argc)
511 mentries_recurse_path(argv[optind++], &real, &settings);
512 } else {
513 mentries_recurse_path(".", &real, &settings);
516 if (!real) {
517 msg(MSG_CRITICAL,
518 "Failed to load metadata from file system\n");
519 exit(EXIT_FAILURE);
522 mentries_compare(real, stored, compare_fix, &settings);
524 if (settings.do_emptydirs)
525 fixup_emptydirs(real, stored);
526 break;
528 case ACTION_HELP:
529 usage(argv[0], NULL);
532 exit(EXIT_SUCCESS);