c20abf9ac87f9d18f8d94ead07e182272cc88141
2 * Main program for agedu.
15 #include <sys/types.h>
19 #include <sys/ioctl.h>
31 #define lenof(x) (sizeof((x))/sizeof(*(x)))
33 void fatal(const char *fmt
, ...)
36 fprintf(stderr
, "%s: ", PNAME
);
38 vfprintf(stderr
, fmt
, ap
);
40 fprintf(stderr
, "\n");
44 struct inclusion_exclusion
{
52 dev_t datafile_dev
, filesystem_dev
;
54 time_t last_output_update
;
55 int progress
, progwidth
;
56 struct inclusion_exclusion
*inex
;
61 static int gotdata(void *vctx
, const char *pathname
, const struct stat64
*st
)
63 struct ctx
*ctx
= (struct ctx
*)vctx
;
64 struct trie_file file
;
70 * Filter out our own data file.
72 if (st
->st_dev
== ctx
->datafile_dev
&& st
->st_ino
== ctx
->datafile_ino
)
76 * Don't cross the streams^W^Wany file system boundary.
78 if (!ctx
->crossfs
&& st
->st_dev
!= ctx
->filesystem_dev
)
82 * Filter based on wildcards.
85 filename
= strrchr(pathname
, '/');
90 for (i
= 0; i
< ctx
->ninex
; i
++) {
91 if (fnmatch(ctx
->inex
[i
].wildcard
,
92 ctx
->inex
[i
].path ? pathname
: filename
,
94 include
= ctx
->inex
[i
].include
;
97 return 1; /* filter, but don't prune */
99 file
.blocks
= st
->st_blocks
;
100 file
.atime
= st
->st_atime
;
101 triebuild_add(ctx
->tb
, pathname
, &file
);
104 if (t
!= ctx
->last_output_update
) {
106 fprintf(stderr
, "%-*.*s\r", ctx
->progwidth
, ctx
->progwidth
,
110 ctx
->last_output_update
= t
;
116 static void text_query(const void *mappedfile
, const char *rootdir
,
121 unsigned long xi1
, xi2
;
122 unsigned long long s1
, s2
;
124 maxpathlen
= trie_maxpathlen(mappedfile
);
125 pathbuf
= snewn(maxpathlen
+ 1, char);
128 * We want to query everything between the supplied filename
129 * (inclusive) and that filename with a ^A on the end
130 * (exclusive). So find the x indices for each.
132 sprintf(pathbuf
, "%s\001", rootdir
);
133 xi1
= trie_before(mappedfile
, rootdir
);
134 xi2
= trie_before(mappedfile
, pathbuf
);
137 * Now do the lookups in the age index.
139 s1
= index_query(mappedfile
, xi1
, t
);
140 s2
= index_query(mappedfile
, xi2
, t
);
142 /* Display in units of 2 512-byte blocks = 1Kb */
143 printf("%-11llu %s\n", (s2
- s1
) / 2, rootdir
);
147 * Now scan for first-level subdirectories and report
152 trie_getpath(mappedfile
, xi1
, pathbuf
);
153 text_query(mappedfile
, pathbuf
, t
, depth
-1);
154 strcat(pathbuf
, "\001");
155 xi1
= trie_before(mappedfile
, pathbuf
);
161 * Largely frivolous way to define all my command-line options. I
162 * present here a parametric macro which declares a series of
163 * _logical_ option identifiers, and for each one declares zero or
164 * more short option characters and zero or more long option
165 * words. Then I repeatedly invoke that macro with its arguments
166 * defined to be various other macros, which allows me to
169 * - define an enum allocating a distinct integer value to each
171 * - define a string consisting of precisely all the short option
173 * - define a string array consisting of all the long option
175 * - define (with help from auxiliary enums) integer arrays
176 * parallel to both of the above giving the logical option id
177 * for each physical short and long option
178 * - define an array indexed by logical option id indicating
179 * whether the option in question takes a value.
181 * It's not at all clear to me that this trickery is actually
182 * particularly _efficient_ - it still, after all, requires going
183 * linearly through the option list at run time and doing a
184 * strcmp, whereas in an ideal world I'd have liked the lists of
185 * long and short options to be pre-sorted so that a binary search
186 * or some other more efficient lookup was possible. (Not that
187 * asymptotic algorithmic complexity is remotely vital in option
188 * parsing, but if I were doing this in, say, Lisp or something
189 * with an equivalently powerful preprocessor then once I'd had
190 * the idea of preparing the option-parsing data structures at
191 * compile time I would probably have made the effort to prepare
192 * them _properly_. I could have Perl generate me a source file
193 * from some sort of description, I suppose, but that would seem
194 * like overkill. And in any case, it's more of a challenge to
195 * achieve as much as possible by cunning use of cpp and enum than
196 * to just write some sensible and logical code in a Turing-
197 * complete language. I said it was largely frivolous :-)
199 * This approach does have the virtue that it brings together the
200 * option ids and option spellings into a single combined list and
201 * defines them all in exactly one place. If I want to add a new
202 * option, or a new spelling for an option, I only have to modify
203 * the main OPTIONS macro below and then add code to process the
206 * (Though, really, even that isn't ideal, since it still involves
207 * modifying the source file in more than one place. In a
208 * _properly_ ideal world, I'd be able to interleave the option
209 * definitions with the code fragments that process them. And then
210 * not bother defining logical identifiers for them at all - those
211 * would be automatically generated, since I wouldn't have any
212 * need to specify them manually in another part of the code.)
215 #define OPTIONS(NOVAL, VAL, SHORT, LONG) \
216 NOVAL(HELP) SHORT(h) LONG(help) \
217 NOVAL(VERSION) SHORT(V) LONG(version) \
218 NOVAL(LICENCE) LONG(licence) LONG(license) \
219 NOVAL(SCAN) SHORT(s) LONG(scan) \
220 NOVAL(DUMP) SHORT(d) LONG(dump) \
221 NOVAL(TEXT) SHORT(t) LONG(text) \
222 NOVAL(HTML) SHORT(H) LONG(html) \
223 NOVAL(HTTPD) SHORT(w) LONG(web) LONG(server) LONG(httpd) \
224 NOVAL(PROGRESS) LONG(progress) LONG(scan_progress) \
225 NOVAL(NOPROGRESS) LONG(no_progress) LONG(no_scan_progress) \
226 NOVAL(TTYPROGRESS) LONG(tty_progress) LONG(tty_scan_progress) \
227 LONG(progress_tty) LONG(scan_progress_tty) \
228 NOVAL(CROSSFS) LONG(cross_fs) \
229 NOVAL(NOCROSSFS) LONG(no_cross_fs) \
230 VAL(DATAFILE) SHORT(f) LONG(file) \
231 VAL(MINAGE) SHORT(a) LONG(age) LONG(min_age) LONG(minimum_age) \
232 VAL(AUTH) LONG(auth) LONG(http_auth) LONG(httpd_auth) \
233 LONG(server_auth) LONG(web_auth) \
234 VAL(INCLUDE) LONG(include) \
235 VAL(INCLUDEPATH) LONG(include_path) \
236 VAL(EXCLUDE) LONG(exclude) \
237 VAL(EXCLUDEPATH) LONG(exclude_path)
240 #define DEFENUM(x) OPT_ ## x,
243 #define STRING(x) #x ,
244 #define STRINGNOCOMMA(x) #x
245 #define SHORTNEWOPT(x) SHORTtmp_ ## x = OPT_ ## x,
246 #define SHORTTHISOPT(x) SHORTtmp2_ ## x, SHORTVAL_ ## x = SHORTtmp2_ ## x - 1,
247 #define SHORTOPTVAL(x) SHORTVAL_ ## x,
248 #define SHORTTMP(x) SHORTtmp3_ ## x,
249 #define LONGNEWOPT(x) LONGtmp_ ## x = OPT_ ## x,
250 #define LONGTHISOPT(x) LONGtmp2_ ## x, LONGVAL_ ## x = LONGtmp2_ ## x - 1,
251 #define LONGOPTVAL(x) LONGVAL_ ## x,
252 #define LONGTMP(x) SHORTtmp3_ ## x,
254 enum { OPTIONS(DEFENUM
,DEFENUM
,IGNORE
,IGNORE
) NOPTIONS
};
255 enum { OPTIONS(IGNORE
,IGNORE
,SHORTTMP
,IGNORE
) NSHORTOPTS
};
256 enum { OPTIONS(IGNORE
,IGNORE
,IGNORE
,LONGTMP
) NLONGOPTS
};
257 static const int opthasval
[NOPTIONS
] = {OPTIONS(ZERO
,ONE
,IGNORE
,IGNORE
)};
258 static const char shortopts
[] = {OPTIONS(IGNORE
,IGNORE
,STRINGNOCOMMA
,IGNORE
)};
259 static const char *const longopts
[] = {OPTIONS(IGNORE
,IGNORE
,IGNORE
,STRING
)};
260 enum { OPTIONS(SHORTNEWOPT
,SHORTNEWOPT
,SHORTTHISOPT
,IGNORE
) };
261 enum { OPTIONS(LONGNEWOPT
,LONGNEWOPT
,IGNORE
,LONGTHISOPT
) };
262 static const int shortvals
[] = {OPTIONS(IGNORE
,IGNORE
,SHORTOPTVAL
,IGNORE
)};
263 static const int longvals
[] = {OPTIONS(IGNORE
,IGNORE
,IGNORE
,LONGOPTVAL
)};
265 int main(int argc
, char **argv
)
268 struct ctx actx
, *ctx
= &actx
;
270 off_t totalsize
, realsize
;
274 const struct trie_file
*tf
;
275 char *filename
= "agedu.dat";
276 char *rootdir
= NULL
;
278 enum { USAGE
, TEXT
, HTML
, SCAN
, DUMP
, HTTPD
} mode
= USAGE
;
280 int auth
= HTTPD_AUTH_MAGIC
| HTTPD_AUTH_BASIC
;
282 struct inclusion_exclusion
*inex
= NULL
;
283 int ninex
= 0, inexsize
= 0;
286 #ifdef DEBUG_MAD_OPTION_PARSING_MACROS
288 static const char *const optnames
[NOPTIONS
] = {
289 OPTIONS(STRING
,STRING
,IGNORE
,IGNORE
)
292 for (i
= 0; i
< NSHORTOPTS
; i
++)
293 printf("-%c == %s [%s]\n", shortopts
[i
], optnames
[shortvals
[i
]],
294 opthasval
[shortvals
[i
]] ?
"value" : "no value");
295 for (i
= 0; i
< NLONGOPTS
; i
++)
296 printf("--%s == %s [%s]\n", longopts
[i
], optnames
[longvals
[i
]],
297 opthasval
[longvals
[i
]] ?
"value" : "no value");
304 if (doing_opts
&& *p
== '-') {
307 if (!strcmp(p
, "--")) {
318 if (wordstart
&& *p
== '-') {
320 * GNU-style long option.
323 optval
= strchr(p
, '=');
327 for (i
= 0; i
< NLONGOPTS
; i
++) {
328 const char *opt
= longopts
[i
], *s
= p
;
331 * The underscores in the option names
332 * defined above may be given by the user
333 * as underscores or dashes, or omitted
338 if (*s
== '-' || *s
== '_')
356 fprintf(stderr
, "%s: unrecognised option '--%s'\n",
361 if (!opthasval
[optid
]) {
363 fprintf(stderr
, "%s: unexpected argument to option"
364 " '--%s'\n", PNAME
, p
);
372 fprintf(stderr
, "%s: option '--%s' expects"
373 " an argument\n", PNAME
, p
);
379 p
+= strlen(p
); /* finished with this argument word */
386 for (i
= 0; i
< NSHORTOPTS
; i
++)
387 if (c
== shortopts
[i
]) {
388 optid
= shortvals
[i
];
393 fprintf(stderr
, "%s: unrecognised option '-%c'\n",
398 if (opthasval
[optid
]) {
402 } else if (--argc
> 0) {
405 fprintf(stderr
, "%s: option '-%c' expects"
406 " an argument\n", PNAME
, c
);
417 * Now actually process the option.
421 printf("FIXME: usage();\n");
424 printf("FIXME: version();\n");
427 printf("FIXME: licence();\n");
450 case OPT_TTYPROGRESS
:
466 if (!strcmp(optval
, "magic"))
467 auth
= HTTPD_AUTH_MAGIC
;
468 else if (!strcmp(optval
, "basic"))
469 auth
= HTTPD_AUTH_BASIC
;
470 else if (!strcmp(optval
, "none"))
471 auth
= HTTPD_AUTH_NONE
;
472 else if (!strcmp(optval
, "default"))
473 auth
= HTTPD_AUTH_MAGIC
| HTTPD_AUTH_BASIC
;
475 fprintf(stderr
, "%s: unrecognised authentication"
476 " type '%s'\n%*s options are 'magic',"
477 " 'basic', 'none', 'default'\n",
478 PNAME
, optval
, (int)strlen(PNAME
), "");
483 case OPT_INCLUDEPATH
:
485 case OPT_EXCLUDEPATH
:
486 if (ninex
>= inexsize
) {
487 inexsize
= ninex
* 3 / 2 + 16;
488 inex
= sresize(inex
, inexsize
,
489 struct inclusion_exclusion
);
491 inex
[ninex
].path
= (optid
== OPT_INCLUDEPATH
||
492 optid
== OPT_EXCLUDEPATH
);
493 inex
[ninex
].include
= (optid
== OPT_INCLUDE
||
494 optid
== OPT_INCLUDEPATH
);
495 inex
[ninex
].wildcard
= optval
;
504 fprintf(stderr
, "%s: unexpected argument '%s'\n", PNAME
, p
);
514 printf("FIXME: usage();\n");
516 } else if (mode
== SCAN
) {
518 fd
= open(filename
, O_RDWR
| O_TRUNC
| O_CREAT
, S_IRWXU
);
520 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
525 if (stat(rootdir
, &st
) < 0) {
526 fprintf(stderr
, "%s: %s: stat: %s\n", PNAME
, rootdir
,
530 ctx
->filesystem_dev
= crossfs ?
0 : st
.st_dev
;
532 if (fstat(fd
, &st
) < 0) {
533 perror("agedu: fstat");
536 ctx
->datafile_dev
= st
.st_dev
;
537 ctx
->datafile_ino
= st
.st_ino
;
540 ctx
->crossfs
= crossfs
;
542 ctx
->last_output_update
= time(NULL
);
544 /* progress==1 means report progress only if stderr is a tty */
546 progress
= isatty(2) ?
2 : 0;
547 ctx
->progress
= progress
;
550 if (progress
&& ioctl(2, TIOCGWINSZ
, &ws
) == 0)
551 ctx
->progwidth
= ws
.ws_col
- 1;
557 * Scan the directory tree, and write out the trie component
560 ctx
->tb
= triebuild_new(fd
);
561 du(rootdir
, gotdata
, ctx
);
562 count
= triebuild_finish(ctx
->tb
);
563 triebuild_free(ctx
->tb
);
566 fprintf(stderr
, "%-*s\r", ctx
->progwidth
, "");
571 * Work out how much space the cumulative index trees will
572 * take; enlarge the file, and memory-map it.
574 if (fstat(fd
, &st
) < 0) {
575 perror("agedu: fstat");
579 printf("Built pathname index, %d entries, %ju bytes\n", count
,
580 (intmax_t)st
.st_size
);
582 totalsize
= index_compute_size(st
.st_size
, count
);
584 if (lseek(fd
, totalsize
-1, SEEK_SET
) < 0) {
585 perror("agedu: lseek");
588 if (write(fd
, "\0", 1) < 1) {
589 perror("agedu: write");
593 printf("Upper bound on index file size = %ju bytes\n",
594 (intmax_t)totalsize
);
596 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
|PROT_WRITE
,MAP_SHARED
, fd
, 0);
598 perror("agedu: mmap");
602 ib
= indexbuild_new(mappedfile
, st
.st_size
, count
);
603 tw
= triewalk_new(mappedfile
);
604 while ((tf
= triewalk_next(tw
, NULL
)) != NULL
)
605 indexbuild_add(ib
, tf
);
607 realsize
= indexbuild_realsize(ib
);
610 munmap(mappedfile
, totalsize
);
611 ftruncate(fd
, realsize
);
613 printf("Actual index file size = %ju bytes\n", (intmax_t)realsize
);
614 } else if (mode
== TEXT
) {
623 if (2 != sscanf(minage
, "%d%1[DdWwMmYy]", &nunits
, unit
)) {
624 fprintf(stderr
, "%s: minimum age should be a number followed by"
625 " one of d,w,m,y\n", PNAME
);
629 if (unit
[0] == 'd') {
631 } else if (unit
[0] == 'w') {
632 t
-= 86400 * 7 * nunits
;
637 ym
= tm
.tm_year
* 12 + tm
.tm_mon
;
644 tm
.tm_year
= ym
/ 12;
650 fd
= open(filename
, O_RDONLY
);
652 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
656 if (fstat(fd
, &st
) < 0) {
657 perror("agedu: fstat");
660 totalsize
= st
.st_size
;
661 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
, MAP_SHARED
, fd
, 0);
663 perror("agedu: mmap");
668 * Trim trailing slash, just in case.
670 pathlen
= strlen(rootdir
);
671 if (pathlen
> 0 && rootdir
[pathlen
-1] == '/')
672 rootdir
[--pathlen
] = '\0';
674 text_query(mappedfile
, rootdir
, t
, 1);
675 } else if (mode
== HTML
) {
680 fd
= open(filename
, O_RDONLY
);
682 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
686 if (fstat(fd
, &st
) < 0) {
687 perror("agedu: fstat");
690 totalsize
= st
.st_size
;
691 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
, MAP_SHARED
, fd
, 0);
693 perror("agedu: mmap");
698 * Trim trailing slash, just in case.
700 pathlen
= strlen(rootdir
);
701 if (pathlen
> 0 && rootdir
[pathlen
-1] == '/')
702 rootdir
[--pathlen
] = '\0';
704 xi
= trie_before(mappedfile
, rootdir
);
705 html
= html_query(mappedfile
, xi
, NULL
);
707 } else if (mode
== DUMP
) {
711 fd
= open(filename
, O_RDONLY
);
713 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
717 if (fstat(fd
, &st
) < 0) {
718 perror("agedu: fstat");
721 totalsize
= st
.st_size
;
722 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
, MAP_SHARED
, fd
, 0);
724 perror("agedu: mmap");
728 maxpathlen
= trie_maxpathlen(mappedfile
);
729 buf
= snewn(maxpathlen
, char);
731 tw
= triewalk_new(mappedfile
);
732 while ((tf
= triewalk_next(tw
, buf
)) != NULL
) {
733 printf("%s: %llu %llu\n", buf
, tf
->blocks
, tf
->atime
);
736 } else if (mode
== HTTPD
) {
737 fd
= open(filename
, O_RDONLY
);
739 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
743 if (fstat(fd
, &st
) < 0) {
744 perror("agedu: fstat");
747 totalsize
= st
.st_size
;
748 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
, MAP_SHARED
, fd
, 0);
750 perror("agedu: mmap");
754 run_httpd(mappedfile
, auth
);