Improve example hooks and remove bashisms in them.
[metastore.git] / metastore.c
blob323525af968296c3548a8743c70f706e50fc0709
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_git = false,
44 /* Used to create lists of dirs / other files which are missing in the fs */
45 static struct metaentry *missingdirs = NULL;
46 static struct metaentry *missingothers = NULL;
49 * Inserts an entry in a linked list ordered by pathlen
51 static void
52 insert_entry_plist(struct metaentry **list, struct metaentry *entry)
54 struct metaentry **parent;
56 for (parent = list; *parent; parent = &((*parent)->list)) {
57 if ((*parent)->pathlen > entry->pathlen)
58 break;
61 entry->list = *parent;
62 *parent = entry;
66 * Prints differences between real and stored actual metadata
67 * - for use in mentries_compare
69 static void
70 compare_print(struct metaentry *real, struct metaentry *stored, int cmp)
72 if (!real && !stored) {
73 msg(MSG_ERROR, "%s called with incorrect arguments\n", __FUNCTION__);
74 return;
77 if (cmp == DIFF_NONE) {
78 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
79 return;
82 msg(MSG_QUIET, "%s:\t", real ? real->path : stored->path);
84 if (cmp & DIFF_ADDED)
85 msg(MSG_QUIET, "added ", real->path);
86 if (cmp & DIFF_DELE)
87 msg(MSG_QUIET, "removed ", stored->path);
88 if (cmp & DIFF_OWNER)
89 msg(MSG_QUIET, "owner ");
90 if (cmp & DIFF_GROUP)
91 msg(MSG_QUIET, "group ");
92 if (cmp & DIFF_MODE)
93 msg(MSG_QUIET, "mode ");
94 if (cmp & DIFF_TYPE)
95 msg(MSG_QUIET, "type ");
96 if (cmp & DIFF_MTIME)
97 msg(MSG_QUIET, "mtime ");
98 if (cmp & DIFF_XATTR)
99 msg(MSG_QUIET, "xattr ");
100 msg(MSG_QUIET, "\n");
104 * Tries to change the real metadata to match the stored one
105 * - for use in mentries_compare
107 static void
108 compare_fix(struct metaentry *real, struct metaentry *stored, int cmp)
110 struct group *group;
111 struct passwd *owner;
112 gid_t gid = -1;
113 uid_t uid = -1;
114 struct utimbuf tbuf;
115 unsigned i;
117 if (!real && !stored) {
118 msg(MSG_ERROR, "%s called with incorrect arguments\n",
119 __FUNCTION__);
120 return;
123 if (!real) {
124 if (S_ISDIR(stored->mode))
125 insert_entry_plist(&missingdirs, stored);
126 else
127 insert_entry_plist(&missingothers, stored);
129 msg(MSG_NORMAL, "%s:\tremoved\n", stored->path);
130 return;
133 if (!stored) {
134 msg(MSG_NORMAL, "%s:\tadded\n", real->path);
135 return;
138 if (cmp == DIFF_NONE) {
139 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
140 return;
143 if (cmp & DIFF_TYPE) {
144 msg(MSG_NORMAL, "%s:\tnew type, will not change metadata\n",
145 real->path);
146 return;
149 msg(MSG_QUIET, "%s:\tchanging metadata\n", real->path);
151 while (cmp & (DIFF_OWNER | DIFF_GROUP)) {
152 if (cmp & DIFF_OWNER) {
153 msg(MSG_NORMAL, "%s:\tchanging owner from %s to %s\n",
154 real->path, real->group, stored->group);
155 owner = xgetpwnam(stored->owner);
156 if (!owner) {
157 msg(MSG_DEBUG, "\tgetpwnam failed: %s\n",
158 strerror(errno));
159 break;
161 uid = owner->pw_uid;
164 if (cmp & DIFF_GROUP) {
165 msg(MSG_NORMAL, "%s:\tchanging group from %s to %s\n",
166 real->path, real->group, stored->group);
167 group = xgetgrnam(stored->group);
168 if (!group) {
169 msg(MSG_DEBUG, "\tgetgrnam failed: %s\n",
170 strerror(errno));
171 break;
173 gid = group->gr_gid;
176 if (lchown(real->path, uid, gid)) {
177 msg(MSG_DEBUG, "\tlchown failed: %s\n",
178 strerror(errno));
179 break;
181 break;
184 if (cmp & DIFF_MODE) {
185 msg(MSG_NORMAL, "%s:\tchanging mode from 0%o to 0%o\n",
186 real->path, real->mode & 07777, stored->mode & 07777);
187 if (chmod(real->path, stored->mode & 07777))
188 msg(MSG_DEBUG, "\tchmod failed: %s\n", strerror(errno));
191 /* FIXME: Use utimensat here, or even better - lutimensat */
192 if ((cmp & DIFF_MTIME) && S_ISLNK(real->mode)) {
193 msg(MSG_NORMAL, "%s:\tsymlink, not changing mtime\n", real->path);
194 } else if (cmp & DIFF_MTIME) {
195 msg(MSG_NORMAL, "%s:\tchanging mtime from %ld to %ld\n",
196 real->path, real->mtime, stored->mtime);
197 tbuf.actime = stored->mtime;
198 tbuf.modtime = stored->mtime;
199 if (utime(real->path, &tbuf)) {
200 msg(MSG_DEBUG, "\tutime failed: %s\n", strerror(errno));
201 return;
205 if (cmp & DIFF_XATTR) {
206 for (i = 0; i < real->xattrs; i++) {
207 /* Any attrs to remove? */
208 if (mentry_find_xattr(stored, real, i) >= 0)
209 continue;
211 msg(MSG_NORMAL, "%s:\tremoving xattr %s\n",
212 real->path, real->xattr_names[i]);
213 if (lremovexattr(real->path, real->xattr_names[i]))
214 msg(MSG_DEBUG, "\tlremovexattr failed: %s\n",
215 strerror(errno));
218 for (i = 0; i < stored->xattrs; i++) {
219 /* Any xattrs to add? (on change they are removed above) */
220 if (mentry_find_xattr(real, stored, i) >= 0)
221 continue;
223 msg(MSG_NORMAL, "%s:\tadding xattr %s\n",
224 stored->path, stored->xattr_names[i]);
225 if (lsetxattr(stored->path, stored->xattr_names[i],
226 stored->xattr_values[i],
227 stored->xattr_lvalues[i], XATTR_CREATE))
228 msg(MSG_DEBUG, "\tlsetxattr failed: %s\n",
229 strerror(errno));
235 * Tries to fix any empty dirs which are missing by recreating them.
236 * An "empty" dir is one which either:
237 * - is empty; or
238 * - only contained empty dirs
240 static void
241 fixup_emptydirs(struct metahash *real, struct metahash *stored)
243 struct metaentry *entry;
244 struct metaentry *cur;
245 struct metaentry **parent;
246 char *bpath;
247 char *delim;
248 size_t blen;
249 struct metaentry *new;
251 if (!missingdirs)
252 return;
253 msg(MSG_DEBUG, "\nAttempting to recreate missing dirs\n");
255 /* If directory x/y is missing, but file x/y/z is also missing,
256 * we should prune directory x/y from the list of directories to
257 * recreate since the deletition of x/y is likely to be genuine
258 * (as opposed to empty dir pruning like git/cvs does).
260 * Also, if file x/y/z is missing, any child directories of
261 * x/y should be pruned as they are probably also intentionally
262 * removed.
265 msg(MSG_DEBUG, "List of candidate dirs:\n");
266 for (cur = missingdirs; cur; cur = cur->list)
267 msg(MSG_DEBUG, " %s\n", cur->path);
269 for (entry = missingothers; entry; entry = entry->list) {
270 msg(MSG_DEBUG, "Pruning using file %s\n", entry->path);
271 bpath = xstrdup(entry->path);
272 delim = strrchr(bpath, '/');
273 if (!delim) {
274 msg(MSG_NORMAL, "No delimiter found in %s\n", bpath);
275 free(bpath);
276 continue;
278 *delim = '\0';
280 parent = &missingdirs;
281 for (cur = *parent; cur; cur = cur->list) {
282 if (strcmp(cur->path, bpath)) {
283 parent = &cur->list;
284 continue;
287 msg(MSG_DEBUG, "Prune phase 1 - %s\n", cur->path);
288 *parent = cur->list;
291 /* Now also prune subdirs of the base dir */
292 *delim++ = '/';
293 *delim = '\0';
294 blen = strlen(bpath);
296 parent = &missingdirs;
297 for (cur = *parent; cur; cur = cur->list) {
298 if (strncmp(cur->path, bpath, blen)) {
299 parent = &cur->list;
300 continue;
303 msg(MSG_DEBUG, "Prune phase 2 - %s\n", cur->path);
304 *parent = cur->list;
307 free(bpath);
309 msg(MSG_DEBUG, "\n");
311 for (cur = missingdirs; cur; cur = cur->list) {
312 msg(MSG_QUIET, "%s:\trecreating...", cur->path);
313 if (mkdir(cur->path, cur->mode)) {
314 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
315 continue;
317 msg(MSG_QUIET, "ok\n");
319 new = mentry_create(cur->path);
320 if (!new) {
321 msg(MSG_QUIET, "Failed to get metadata for %s\n");
322 continue;
325 compare_fix(new, cur, mentry_compare(new, cur, &settings));
329 /* Prints usage message and exits */
330 static void
331 usage(const char *arg0, const char *message)
333 if (message)
334 msg(MSG_CRITICAL, "%s: %s\n\n", arg0, message);
335 msg(MSG_CRITICAL, "Usage: %s ACTION [OPTION...] [PATH...]\n\n", arg0);
336 msg(MSG_CRITICAL, "Where ACTION is one of:\n"
337 " -c, --compare\t\tShow differences between stored and real metadata\n"
338 " -s, --save\t\tSave current metadata\n"
339 " -a, --apply\t\tApply stored metadata\n"
340 " -h, --help\t\tHelp message (this text)\n\n"
341 "Valid OPTIONS are:\n"
342 " -v, --verbose\t\tPrint more verbose messages\n"
343 " -q, --quiet\t\tPrint less verbose messages\n"
344 " -m, --mtime\t\tAlso take mtime into account for diff or apply\n"
345 " -e, --empty-dirs\tRecreate missing empty directories (experimental)\n"
346 " -g, --git\t\tDo not omit .git directories\n"
347 " -f, --file <file>\tSet metadata file\n"
350 exit(message ? EXIT_FAILURE : EXIT_SUCCESS);
353 /* Options */
354 static struct option long_options[] = {
355 {"compare", 0, 0, 0},
356 {"save", 0, 0, 0},
357 {"apply", 0, 0, 0},
358 {"help", 0, 0, 0},
359 {"verbose", 0, 0, 0},
360 {"quiet", 0, 0, 0},
361 {"mtime", 0, 0, 0},
362 {"empty-dirs", 0, 0, 0},
363 {"git", 0, 0, 0},
364 {"file", required_argument, 0, 0},
365 {0, 0, 0, 0}
368 /* Main function */
370 main(int argc, char **argv, char **envp)
372 int i, c;
373 struct metahash *real = NULL;
374 struct metahash *stored = NULL;
375 int action = 0;
377 /* Parse options */
378 i = 0;
379 while (1) {
380 int option_index = 0;
381 c = getopt_long(argc, argv, "csahvqmegf:",
382 long_options, &option_index);
383 if (c == -1)
384 break;
385 switch (c) {
386 case 0:
387 if (!strcmp("verbose",
388 long_options[option_index].name)) {
389 adjust_verbosity(1);
390 } else if (!strcmp("quiet",
391 long_options[option_index].name)) {
392 adjust_verbosity(-1);
393 } else if (!strcmp("mtime",
394 long_options[option_index].name)) {
395 settings.do_mtime = true;
396 } else if (!strcmp("empty-dirs",
397 long_options[option_index].name)) {
398 settings.do_emptydirs = true;
399 } else if (!strcmp("git",
400 long_options[option_index].name)) {
401 settings.do_git = true;
402 } else if (!strcmp("file",
403 long_options[option_index].name)) {
404 settings.metafile = optarg;
405 } else {
406 action |= (1 << option_index);
407 i++;
409 break;
410 case 'c':
411 action |= ACTION_DIFF;
412 i++;
413 break;
414 case 's':
415 action |= ACTION_SAVE;
416 i++;
417 break;
418 case 'a':
419 action |= ACTION_APPLY;
420 i++;
421 break;
422 case 'h':
423 action |= ACTION_HELP;
424 i++;
425 break;
426 case 'v':
427 adjust_verbosity(1);
428 break;
429 case 'q':
430 adjust_verbosity(-1);
431 break;
432 case 'm':
433 settings.do_mtime = true;
434 break;
435 case 'e':
436 settings.do_emptydirs = true;
437 break;
438 case 'g':
439 settings.do_git = true;
440 break;
441 case 'f':
442 settings.metafile = optarg;
443 break;
444 default:
445 usage(argv[0], "unknown option");
449 /* Make sure only one action is specified */
450 if (i != 1)
451 usage(argv[0], "incorrect option(s)");
453 /* Make sure --empty-dirs is only used with apply */
454 if (settings.do_emptydirs && action != ACTION_APPLY)
455 usage(argv[0], "--empty-dirs is only valid with --apply");
457 /* Perform action */
458 switch (action) {
459 case ACTION_DIFF:
460 mentries_fromfile(&stored, settings.metafile);
461 if (!stored) {
462 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
463 settings.metafile);
464 exit(EXIT_FAILURE);
467 if (optind < argc) {
468 while (optind < argc)
469 mentries_recurse_path(argv[optind++], &real, &settings);
470 } else {
471 mentries_recurse_path(".", &real, &settings);
474 if (!real) {
475 msg(MSG_CRITICAL,
476 "Failed to load metadata from file system\n");
477 exit(EXIT_FAILURE);
480 mentries_compare(real, stored, compare_print, &settings);
481 break;
483 case ACTION_SAVE:
484 if (optind < argc) {
485 while (optind < argc)
486 mentries_recurse_path(argv[optind++], &real, &settings);
487 } else {
488 mentries_recurse_path(".", &real, &settings);
491 if (!real) {
492 msg(MSG_CRITICAL,
493 "Failed to load metadata from file system\n");
494 exit(EXIT_FAILURE);
497 mentries_tofile(real, settings.metafile);
498 break;
500 case ACTION_APPLY:
501 mentries_fromfile(&stored, settings.metafile);
502 if (!stored) {
503 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
504 settings.metafile);
505 exit(EXIT_FAILURE);
508 if (optind < argc) {
509 while (optind < argc)
510 mentries_recurse_path(argv[optind++], &real, &settings);
511 } else {
512 mentries_recurse_path(".", &real, &settings);
515 if (!real) {
516 msg(MSG_CRITICAL,
517 "Failed to load metadata from file system\n");
518 exit(EXIT_FAILURE);
521 mentries_compare(real, stored, compare_fix, &settings);
523 if (settings.do_emptydirs)
524 fixup_emptydirs(real, stored);
525 break;
527 case ACTION_HELP:
528 usage(argv[0], NULL);
531 exit(EXIT_SUCCESS);