2 * Main program for agedu.
16 * Path separator. This global variable affects the behaviour of
17 * various parts of the code when they need to deal with path
18 * separators. The path separator appropriate to a particular data
19 * set is encoded in the index file storing that data set; data
20 * sets generated on Unix will of course have the default '/', but
21 * foreign data sets are conceivable and must be handled correctly.
25 void fatal(const char *fmt
, ...)
28 fprintf(stderr
, "%s: ", PNAME
);
30 vfprintf(stderr
, fmt
, ap
);
32 fprintf(stderr
, "\n");
36 struct inclusion_exclusion
{
44 dev_t datafile_dev
, filesystem_dev
;
46 time_t last_output_update
;
47 int progress
, progwidth
;
49 struct inclusion_exclusion
*inex
;
56 static void dump_line(const char *pathname
, const struct trie_file
*tf
)
59 if (printf("%llu %llu ", tf
->size
, tf
->atime
) < 0) goto error
;
60 for (p
= pathname
; *p
; p
++) {
61 if (*p
>= ' ' && *p
< 127 && *p
!= '%') {
62 if (putchar(*p
) == EOF
) goto error
;
64 if (printf("%%%02x", (unsigned char)*p
) < 0) goto error
;
67 if (putchar('\n') == EOF
) goto error
;
70 fatal("standard output: %s", strerror(errno
));
73 static int gotdata(void *vctx
, const char *pathname
, const STRUCT_STAT
*st
)
75 struct ctx
*ctx
= (struct ctx
*)vctx
;
76 struct trie_file file
;
82 * Filter out our own data file.
84 if (st
->st_dev
== ctx
->datafile_dev
&& st
->st_ino
== ctx
->datafile_ino
)
88 * Don't cross the streams^W^Wany file system boundary.
90 if (!ctx
->crossfs
&& st
->st_dev
!= ctx
->filesystem_dev
)
93 file
.size
= (unsigned long long)512 * st
->st_blocks
;
94 if (ctx
->usemtime
|| (ctx
->fakeatimes
&& S_ISDIR(st
->st_mode
)))
95 file
.atime
= st
->st_mtime
;
97 file
.atime
= max(st
->st_mtime
, st
->st_atime
);
100 * Filter based on wildcards.
103 filename
= strrchr(pathname
, pathsep
);
108 for (i
= 0; i
< ctx
->ninex
; i
++) {
109 if (fnmatch(ctx
->inex
[i
].wildcard
,
110 ctx
->inex
[i
].path ? pathname
: filename
, 0) == 0)
111 include
= ctx
->inex
[i
].type
;
114 return 0; /* ignore this entry and any subdirs */
117 * Here we are supposed to be filtering an entry out, but
118 * still recursing into it if it's a directory. However,
119 * we can't actually leave out any directory whose
120 * subdirectories we then look at. So we cheat, in that
121 * case, by setting the size to zero.
123 if (!S_ISDIR(st
->st_mode
))
124 return 0; /* just ignore */
129 if (ctx
->straight_to_dump
)
130 dump_line(pathname
, &file
);
132 triebuild_add(ctx
->tb
, pathname
, &file
);
136 if (t
!= ctx
->last_output_update
) {
137 fprintf(stderr
, "%-*.*s\r", ctx
->progwidth
, ctx
->progwidth
,
140 ctx
->last_output_update
= t
;
147 static void scan_error(void *vctx
, const char *fmt
, ...)
149 struct ctx
*ctx
= (struct ctx
*)vctx
;
153 fprintf(stderr
, "%-*s\r", ctx
->progwidth
, "");
157 fprintf(stderr
, "%s: ", PNAME
);
159 vfprintf(stderr
, fmt
, ap
);
162 ctx
->last_output_update
--; /* force a progress report next time */
165 static void text_query(const void *mappedfile
, const char *querydir
,
166 time_t t
, int showfiles
, int depth
, FILE *fp
)
170 unsigned long xi1
, xi2
;
171 unsigned long long size
;
173 maxpathlen
= trie_maxpathlen(mappedfile
);
174 pathbuf
= snewn(maxpathlen
+ 1, char);
177 * We want to query everything between the supplied filename
178 * (inclusive) and that filename with a ^A on the end
179 * (exclusive). So find the x indices for each.
181 strcpy(pathbuf
, querydir
);
182 make_successor(pathbuf
);
183 xi1
= trie_before(mappedfile
, querydir
);
184 xi2
= trie_before(mappedfile
, pathbuf
);
186 if (!showfiles
&& xi2
- xi1
== 1)
187 return; /* file, or empty dir => no display */
190 * Now do the lookups in the age index.
192 if (xi2
- xi1
== 1) {
194 * We are querying an individual file, so we should not
195 * depend on the index entries either side of the node,
196 * since they almost certainly don't both exist. Instead,
197 * just look up the file's size and atime in the main trie.
199 const struct trie_file
*f
= trie_getfile(mappedfile
, xi1
);
205 unsigned long long s1
, s2
;
206 s1
= index_query(mappedfile
, xi1
, t
);
207 s2
= index_query(mappedfile
, xi2
, t
);
212 return; /* no space taken up => no display */
216 * Now scan for first-level subdirectories and report
219 int newdepth
= (depth
> 0 ? depth
- 1 : depth
);
222 trie_getpath(mappedfile
, xi1
, pathbuf
);
223 text_query(mappedfile
, pathbuf
, t
, showfiles
, newdepth
, fp
);
224 make_successor(pathbuf
);
225 xi1
= trie_before(mappedfile
, pathbuf
);
229 /* Display in units of 1Kb */
230 fprintf(fp
, "%-11llu %s\n", (size
) / 1024, querydir
);
234 * Largely frivolous way to define all my command-line options. I
235 * present here a parametric macro which declares a series of
236 * _logical_ option identifiers, and for each one declares zero or
237 * more short option characters and zero or more long option
238 * words. Then I repeatedly invoke that macro with its arguments
239 * defined to be various other macros, which allows me to
242 * - define an enum allocating a distinct integer value to each
244 * - define a string consisting of precisely all the short option
246 * - define a string array consisting of all the long option
248 * - define (with help from auxiliary enums) integer arrays
249 * parallel to both of the above giving the logical option id
250 * for each physical short and long option
251 * - define an array indexed by logical option id indicating
252 * whether the option in question takes a value
253 * - define a function which prints out brief online help for all
256 * It's not at all clear to me that this trickery is actually
257 * particularly _efficient_ - it still, after all, requires going
258 * linearly through the option list at run time and doing a
259 * strcmp, whereas in an ideal world I'd have liked the lists of
260 * long and short options to be pre-sorted so that a binary search
261 * or some other more efficient lookup was possible. (Not that
262 * asymptotic algorithmic complexity is remotely vital in option
263 * parsing, but if I were doing this in, say, Lisp or something
264 * with an equivalently powerful preprocessor then once I'd had
265 * the idea of preparing the option-parsing data structures at
266 * compile time I would probably have made the effort to prepare
267 * them _properly_. I could have Perl generate me a source file
268 * from some sort of description, I suppose, but that would seem
269 * like overkill. And in any case, it's more of a challenge to
270 * achieve as much as possible by cunning use of cpp and enum than
271 * to just write some sensible and logical code in a Turing-
272 * complete language. I said it was largely frivolous :-)
274 * This approach does have the virtue that it brings together the
275 * option ids, option spellings and help text into a single
276 * combined list and defines them all in exactly one place. If I
277 * want to add a new option, or a new spelling for an option, I
278 * only have to modify the main OPTHELP macro below and then add
279 * code to process the new logical id.
281 * (Though, really, even that isn't ideal, since it still involves
282 * modifying the source file in more than one place. In a
283 * _properly_ ideal world, I'd be able to interleave the option
284 * definitions with the code fragments that process them. And then
285 * not bother defining logical identifiers for them at all - those
286 * would be automatically generated, since I wouldn't have any
287 * need to specify them manually in another part of the code.)
289 * One other helpful consequence of the enum-based structure here
290 * is that it causes a compiler error if I accidentally try to
291 * define the same option (short or long) twice.
294 #define OPTHELP(NOVAL, VAL, SHORT, LONG, HELPPFX, HELPARG, HELPLINE, HELPOPT) \
295 HELPPFX("usage") HELPLINE(PNAME " [options] action [action...]") \
297 VAL(SCAN) SHORT(s) LONG(scan) \
298 HELPARG("directory") HELPOPT("scan and index a directory") \
299 NOVAL(HTTPD) SHORT(w) LONG(web) LONG(server) LONG(httpd) \
300 HELPOPT("serve HTML reports from a temporary web server") \
301 VAL(TEXT) SHORT(t) LONG(text) \
302 HELPARG("subdir") HELPOPT("print a plain text report on a subdirectory") \
303 NOVAL(REMOVE) SHORT(R) LONG(remove) LONG(delete) LONG(unlink) \
304 HELPOPT("remove the index file") \
305 NOVAL(DUMP) SHORT(D) LONG(dump) HELPOPT("dump the index file on stdout") \
306 NOVAL(LOAD) SHORT(L) LONG(load) \
307 HELPOPT("load and index a dump file") \
308 VAL(SCANDUMP) SHORT(S) LONG(scan_dump) \
309 HELPARG("directory") HELPOPT("scan only, generating a dump") \
310 VAL(HTML) SHORT(H) LONG(html) \
311 HELPARG("subdir") HELPOPT("print an HTML report on a subdirectory") \
312 NOVAL(CGI) LONG(cgi) \
313 HELPOPT("do the right thing when run from a CGI script") \
315 VAL(DATAFILE) SHORT(f) LONG(file) \
316 HELPARG("filename") HELPOPT("[most modes] specify index file") \
317 NOVAL(CROSSFS) LONG(cross_fs) \
318 HELPOPT("[--scan] cross filesystem boundaries") \
319 NOVAL(NOCROSSFS) LONG(no_cross_fs) \
320 HELPOPT("[--scan] stick to one filesystem") \
321 VAL(PRUNE) LONG(prune) \
322 HELPARG("wildcard") HELPOPT("[--scan] prune files matching pattern") \
323 VAL(PRUNEPATH) LONG(prune_path) \
324 HELPARG("wildcard") HELPOPT("[--scan] prune pathnames matching pattern") \
325 VAL(EXCLUDE) LONG(exclude) \
326 HELPARG("wildcard") HELPOPT("[--scan] exclude files matching pattern") \
327 VAL(EXCLUDEPATH) LONG(exclude_path) \
328 HELPARG("wildcard") HELPOPT("[--scan] exclude pathnames matching pattern") \
329 VAL(INCLUDE) LONG(include) \
330 HELPARG("wildcard") HELPOPT("[--scan] include files matching pattern") \
331 VAL(INCLUDEPATH) LONG(include_path) \
332 HELPARG("wildcard") HELPOPT("[--scan] include pathnames matching pattern") \
333 NOVAL(PROGRESS) LONG(progress) LONG(scan_progress) \
334 HELPOPT("[--scan] report progress on stderr") \
335 NOVAL(NOPROGRESS) LONG(no_progress) LONG(no_scan_progress) \
336 HELPOPT("[--scan] do not report progress") \
337 NOVAL(TTYPROGRESS) LONG(tty_progress) LONG(tty_scan_progress) \
338 LONG(progress_tty) LONG(scan_progress_tty) \
339 HELPOPT("[--scan] report progress if stderr is a tty") \
340 NOVAL(DIRATIME) LONG(dir_atime) LONG(dir_atimes) \
341 HELPOPT("[--scan,--load] keep real atimes on directories") \
342 NOVAL(NODIRATIME) LONG(no_dir_atime) LONG(no_dir_atimes) \
343 HELPOPT("[--scan,--load] fake atimes on directories") \
344 NOVAL(NOEOF) LONG(no_eof) LONG(noeof) \
345 HELPOPT("[--web] do not close web server on EOF") \
346 NOVAL(MTIME) LONG(mtime) \
347 HELPOPT("[--scan] use mtime instead of atime") \
348 NOVAL(SHOWFILES) LONG(files) \
349 HELPOPT("[--web,--html,--text] list individual files") \
350 VAL(AGERANGE) SHORT(r) LONG(age_range) LONG(range) LONG(ages) \
351 HELPARG("age[-age]") HELPOPT("[--web,--html] set limits of colour coding") \
352 VAL(OUTFILE) SHORT(o) LONG(output) \
353 HELPARG("filename") HELPOPT("[--html] specify output file or directory name") \
354 VAL(SERVERADDR) LONG(address) LONG(addr) LONG(server_address) \
356 HELPARG("addr[:port]") HELPOPT("[--web] specify HTTP server address") \
357 VAL(AUTH) LONG(auth) LONG(http_auth) LONG(httpd_auth) \
358 LONG(server_auth) LONG(web_auth) \
359 HELPARG("type") HELPOPT("[--web] specify HTTP authentication method") \
360 VAL(AUTHFILE) LONG(auth_file) \
361 HELPARG("filename") HELPOPT("[--web] read HTTP Basic user/pass from file") \
362 VAL(AUTHFD) LONG(auth_fd) \
363 HELPARG("fd") HELPOPT("[--web] read HTTP Basic user/pass from fd") \
364 VAL(HTMLTITLE) LONG(title) \
365 HELPARG("title") HELPOPT("[--web,--html] title prefix for web pages") \
366 VAL(DEPTH) SHORT(d) LONG(depth) LONG(max_depth) LONG(maximum_depth) \
367 HELPARG("levels") HELPOPT("[--text,--html] recurse to this many levels") \
368 VAL(MINAGE) SHORT(a) LONG(age) LONG(min_age) LONG(minimum_age) \
369 HELPARG("age") HELPOPT("[--text] include only files older than this") \
371 NOVAL(HELP) SHORT(h) LONG(help) HELPOPT("display this help text") \
372 NOVAL(VERSION) SHORT(V) LONG(version) HELPOPT("report version number") \
373 NOVAL(LICENCE) LONG(licence) LONG(license) \
374 HELPOPT("display (MIT) licence text") \
377 #define DEFENUM(x) OPT_ ## x,
380 #define STRING(x) #x ,
381 #define STRINGNOCOMMA(x) #x
382 #define SHORTNEWOPT(x) SHORTtmp_ ## x = OPT_ ## x,
383 #define SHORTTHISOPT(x) SHORTtmp2_ ## x, SHORTVAL_ ## x = SHORTtmp2_ ## x - 1,
384 #define SHORTOPTVAL(x) SHORTVAL_ ## x,
385 #define SHORTTMP(x) SHORTtmp3_ ## x,
386 #define LONGNEWOPT(x) LONGtmp_ ## x = OPT_ ## x,
387 #define LONGTHISOPT(x) LONGtmp2_ ## x, LONGVAL_ ## x = LONGtmp2_ ## x - 1,
388 #define LONGOPTVAL(x) LONGVAL_ ## x,
389 #define LONGTMP(x) SHORTtmp3_ ## x,
391 #define OPTIONS(NOVAL, VAL, SHORT, LONG) \
392 OPTHELP(NOVAL, VAL, SHORT, LONG, IGNORE, IGNORE, IGNORE, IGNORE)
394 enum { OPTIONS(DEFENUM
,DEFENUM
,IGNORE
,IGNORE
) NOPTIONS
};
395 enum { OPTIONS(IGNORE
,IGNORE
,SHORTTMP
,IGNORE
) NSHORTOPTS
};
396 enum { OPTIONS(IGNORE
,IGNORE
,IGNORE
,LONGTMP
) NLONGOPTS
};
397 static const int opthasval
[NOPTIONS
] = {OPTIONS(ZERO
,ONE
,IGNORE
,IGNORE
)};
398 static const char shortopts
[] = {OPTIONS(IGNORE
,IGNORE
,STRINGNOCOMMA
,IGNORE
)};
399 static const char *const longopts
[] = {OPTIONS(IGNORE
,IGNORE
,IGNORE
,STRING
)};
400 enum { OPTIONS(SHORTNEWOPT
,SHORTNEWOPT
,SHORTTHISOPT
,IGNORE
) UNUSEDENUMVAL1
};
401 enum { OPTIONS(LONGNEWOPT
,LONGNEWOPT
,IGNORE
,LONGTHISOPT
) UNUSEDENUMVAL2
};
402 static const int shortvals
[] = {OPTIONS(IGNORE
,IGNORE
,SHORTOPTVAL
,IGNORE
)};
403 static const int longvals
[] = {OPTIONS(IGNORE
,IGNORE
,IGNORE
,LONGOPTVAL
)};
405 static void usage(FILE *fp
)
408 const char *prefix
, *shortopt
, *longopt
, *optarg
;
411 #define HELPRESET prefix = shortopt = longopt = optarg = NULL, optex = -1
412 #define HELPNOVAL(s) optex = 0;
413 #define HELPVAL(s) optex = 1;
414 #define HELPSHORT(s) if (!shortopt) shortopt = "-" #s;
415 #define HELPLONG(s) if (!longopt) { \
416 strcpy(longbuf, "--" #s); longopt = longbuf; \
417 for (i = 0; longbuf[i]; i++) if (longbuf[i] == '_') longbuf[i] = '-'; }
418 #define HELPPFX(s) prefix = s;
419 #define HELPARG(s) optarg = s;
420 #define HELPLINE(s) assert(optex == -1); \
421 fprintf(fp, "%7s%c %s\n", prefix?prefix:"", prefix?':':' ', s); \
423 #define HELPOPT(s) assert((optex == 1 && optarg) || (optex == 0 && !optarg)); \
424 assert(shortopt || longopt); \
425 i = fprintf(fp, "%7s%c %s%s%s%s%s", prefix?prefix:"", prefix?':':' ', \
426 shortopt?shortopt:"", shortopt&&longopt?", ":"", longopt?longopt:"", \
427 optarg?" ":"", optarg?optarg:""); \
428 fprintf(fp, "%*s %s\n", i<32?32-i:0,"",s); HELPRESET;
431 OPTHELP(HELPNOVAL
, HELPVAL
, HELPSHORT
, HELPLONG
,
432 HELPPFX
, HELPARG
, HELPLINE
, HELPOPT
);
445 static time_t parse_age(time_t now
, const char *agestr
)
454 if (2 != sscanf(agestr
, "%d%1[DdWwMmYy]", &nunits
, unit
)) {
455 fprintf(stderr
, "%s: age specification should be a number followed by"
456 " one of d,w,m,y\n", PNAME
);
460 if (unit
[0] == 'd') {
462 } else if (unit
[0] == 'w') {
463 t
-= 86400 * 7 * nunits
;
468 ym
= tm
.tm_year
* 12 + tm
.tm_mon
;
475 tm
.tm_year
= ym
/ 12;
484 int main(int argc
, char **argv
)
487 struct ctx actx
, *ctx
= &actx
;
489 off_t totalsize
, realsize
;
493 const struct trie_file
*tf
, *prevtf
;
494 char *filename
= PNAME
".dat";
496 enum { TEXT
, HTML
, SCAN
, DUMP
, SCANDUMP
, LOAD
, HTTPD
, REMOVE
};
501 int nactions
= 0, actionsize
= 0, action
;
502 time_t now
= time(NULL
);
503 time_t textcutoff
= now
, htmlnewest
= now
, htmloldest
= now
;
504 int htmlautoagerange
= 1;
505 const char *httpserveraddr
= NULL
;
506 int httpserverport
= 0;
507 const char *httpauthdata
= NULL
;
508 const char *outfile
= NULL
;
509 const char *html_title
= PNAME
;
510 int auth
= HTTPD_AUTH_MAGIC
| HTTPD_AUTH_BASIC
;
512 struct inclusion_exclusion
*inex
= NULL
;
513 int ninex
= 0, inexsize
= 0;
515 int depth
= -1, gotdepth
= 0;
516 int fakediratimes
= 1;
521 #ifdef DEBUG_MAD_OPTION_PARSING_MACROS
523 static const char *const optnames
[NOPTIONS
] = {
524 OPTIONS(STRING
,STRING
,IGNORE
,IGNORE
)
527 for (i
= 0; i
< NSHORTOPTS
; i
++)
528 printf("-%c == %s [%s]\n", shortopts
[i
], optnames
[shortvals
[i
]],
529 opthasval
[shortvals
[i
]] ?
"value" : "no value");
530 for (i
= 0; i
< NLONGOPTS
; i
++)
531 printf("--%s == %s [%s]\n", longopts
[i
], optnames
[longvals
[i
]],
532 opthasval
[longvals
[i
]] ?
"value" : "no value");
539 if (doing_opts
&& *p
== '-') {
542 if (!strcmp(p
, "--")) {
553 if (wordstart
&& *p
== '-') {
555 * GNU-style long option.
558 optval
= strchr(p
, '=');
562 for (i
= 0; i
< NLONGOPTS
; i
++) {
563 const char *opt
= longopts
[i
], *s
= p
;
566 * The underscores in the option names
567 * defined above may be given by the user
568 * as underscores or dashes, or omitted
573 if (*s
== '-' || *s
== '_')
591 fprintf(stderr
, "%s: unrecognised option '--%s'\n",
596 if (!opthasval
[optid
]) {
598 fprintf(stderr
, "%s: unexpected argument to option"
599 " '--%s'\n", PNAME
, p
);
607 fprintf(stderr
, "%s: option '--%s' expects"
608 " an argument\n", PNAME
, p
);
614 p
+= strlen(p
); /* finished with this argument word */
621 for (i
= 0; i
< NSHORTOPTS
; i
++)
622 if (c
== shortopts
[i
]) {
623 optid
= shortvals
[i
];
628 fprintf(stderr
, "%s: unrecognised option '-%c'\n",
633 if (opthasval
[optid
]) {
637 } else if (--argc
> 0) {
640 fprintf(stderr
, "%s: option '-%c' expects"
641 " an argument\n", PNAME
, c
);
652 * Now actually process the option.
659 #ifdef PACKAGE_VERSION
660 printf("%s, revision %s\n", PNAME
, PACKAGE_VERSION
);
662 printf("%s: version number not available when not built"
663 " via automake\n", PNAME
);
668 extern const char *const licence
[];
671 for (i
= 0; licence
[i
]; i
++)
672 fputs(licence
[i
], stdout
);
676 if (nactions
>= actionsize
) {
677 actionsize
= nactions
* 3 / 2 + 16;
678 actions
= sresize(actions
, actionsize
, struct action
);
680 actions
[nactions
].mode
= SCAN
;
681 actions
[nactions
].arg
= optval
;
685 if (nactions
>= actionsize
) {
686 actionsize
= nactions
* 3 / 2 + 16;
687 actions
= sresize(actions
, actionsize
, struct action
);
689 actions
[nactions
].mode
= SCANDUMP
;
690 actions
[nactions
].arg
= optval
;
694 if (nactions
>= actionsize
) {
695 actionsize
= nactions
* 3 / 2 + 16;
696 actions
= sresize(actions
, actionsize
, struct action
);
698 actions
[nactions
].mode
= DUMP
;
699 actions
[nactions
].arg
= NULL
;
703 if (nactions
>= actionsize
) {
704 actionsize
= nactions
* 3 / 2 + 16;
705 actions
= sresize(actions
, actionsize
, struct action
);
707 actions
[nactions
].mode
= LOAD
;
708 actions
[nactions
].arg
= NULL
;
712 if (nactions
>= actionsize
) {
713 actionsize
= nactions
* 3 / 2 + 16;
714 actions
= sresize(actions
, actionsize
, struct action
);
716 actions
[nactions
].mode
= TEXT
;
717 actions
[nactions
].arg
= optval
;
722 if (nactions
>= actionsize
) {
723 actionsize
= nactions
* 3 / 2 + 16;
724 actions
= sresize(actions
, actionsize
, struct action
);
726 actions
[nactions
].mode
= HTML
;
727 actions
[nactions
].arg
= (optid
== OPT_HTML ? optval
:
732 if (nactions
>= actionsize
) {
733 actionsize
= nactions
* 3 / 2 + 16;
734 actions
= sresize(actions
, actionsize
, struct action
);
736 actions
[nactions
].mode
= HTTPD
;
737 actions
[nactions
].arg
= NULL
;
741 if (nactions
>= actionsize
) {
742 actionsize
= nactions
* 3 / 2 + 16;
743 actions
= sresize(actions
, actionsize
, struct action
);
745 actions
[nactions
].mode
= REMOVE
;
746 actions
[nactions
].arg
= NULL
;
755 case OPT_TTYPROGRESS
:
783 if (!strcasecmp(optval
, "unlimited") ||
784 !strcasecmp(optval
, "infinity") ||
785 !strcasecmp(optval
, "infinite") ||
786 !strcasecmp(optval
, "inf") ||
787 !strcasecmp(optval
, "maximum") ||
788 !strcasecmp(optval
, "max"))
791 depth
= atoi(optval
);
801 textcutoff
= parse_age(now
, optval
);
804 if (!strcmp(optval
, "auto")) {
805 htmlautoagerange
= 1;
807 char *q
= optval
+ strcspn(optval
, "-:");
810 htmloldest
= parse_age(now
, optval
);
811 htmlnewest
= *q ?
parse_age(now
, q
) : now
;
812 htmlautoagerange
= 0;
818 if (optval
[0] == '[' &&
819 (port
= strchr(optval
, ']')) != NULL
)
823 port
+= strcspn(port
, ":");
826 httpserveraddr
= optval
;
827 httpserverport
= atoi(port
);
831 if (!strcmp(optval
, "magic"))
832 auth
= HTTPD_AUTH_MAGIC
;
833 else if (!strcmp(optval
, "basic"))
834 auth
= HTTPD_AUTH_BASIC
;
835 else if (!strcmp(optval
, "none"))
836 auth
= HTTPD_AUTH_NONE
;
837 else if (!strcmp(optval
, "default"))
838 auth
= HTTPD_AUTH_MAGIC
| HTTPD_AUTH_BASIC
;
839 else if (!strcmp(optval
, "help") ||
840 !strcmp(optval
, "list")) {
841 printf(PNAME
": supported HTTP authentication types"
843 " magic use Linux /proc/net/tcp to"
844 " determine owner of peer socket\n"
845 " basic HTTP Basic username and"
846 " password authentication\n"
847 " default use 'magic' if possible, "
848 " otherwise fall back to 'basic'\n"
849 " none unauthenticated HTTP (if"
850 " the data file is non-confidential)\n");
853 fprintf(stderr
, "%s: unrecognised authentication"
854 " type '%s'\n%*s options are 'magic',"
855 " 'basic', 'none', 'default'\n",
856 PNAME
, optval
, (int)strlen(PNAME
), "");
867 int authlen
, authsize
;
870 if (optid
== OPT_AUTHFILE
) {
871 fd
= open(optval
, O_RDONLY
);
873 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
,
874 optval
, strerror(errno
));
881 sprintf(namebuf
, "fd %d", fd
);
886 authbuf
= snewn(authsize
, char);
887 while ((ret
= read(fd
, authbuf
+authlen
,
888 authsize
-authlen
)) > 0) {
890 if ((authsize
- authlen
) < (authsize
/ 16)) {
891 authsize
= authlen
* 3 / 2 + 4096;
892 authbuf
= sresize(authbuf
, authsize
, char);
896 fprintf(stderr
, "%s: %s: read: %s\n", PNAME
,
897 name
, strerror(errno
));
900 if (optid
== OPT_AUTHFILE
)
902 httpauthdata
= authbuf
;
906 case OPT_INCLUDEPATH
:
908 case OPT_EXCLUDEPATH
:
911 if (ninex
>= inexsize
) {
912 inexsize
= ninex
* 3 / 2 + 16;
913 inex
= sresize(inex
, inexsize
,
914 struct inclusion_exclusion
);
916 inex
[ninex
].path
= (optid
== OPT_INCLUDEPATH
||
917 optid
== OPT_EXCLUDEPATH
||
918 optid
== OPT_PRUNEPATH
);
919 inex
[ninex
].type
= (optid
== OPT_INCLUDE ?
1 :
920 optid
== OPT_INCLUDEPATH ?
1 :
921 optid
== OPT_EXCLUDE ?
0 :
922 optid
== OPT_EXCLUDEPATH ?
0 :
923 optid
== OPT_PRUNE ?
-1 :
924 /* optid == OPT_PRUNEPATH ? */ -1);
925 inex
[ninex
].wildcard
= optval
;
931 fprintf(stderr
, "%s: unexpected argument '%s'\n", PNAME
, p
);
941 for (action
= 0; action
< nactions
; action
++) {
942 int mode
= actions
[action
].mode
;
944 if (mode
== SCAN
|| mode
== SCANDUMP
|| mode
== LOAD
) {
945 const char *scandir
= actions
[action
].arg
;
948 char *buf
= fgetline(stdin
);
950 buf
[strcspn(buf
, "\r\n")] = '\0';
951 if (1 != sscanf(buf
, DUMPHDR
"%x",
953 fprintf(stderr
, "%s: header in dump file not recognised\n",
957 pathsep
= (char)newpathsep
;
961 if (mode
== SCAN
|| mode
== LOAD
) {
963 * Prepare to write out the index file.
965 fd
= open(filename
, O_RDWR
| O_TRUNC
| O_CREAT
,
968 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
972 if (fstat(fd
, &st
) < 0) {
973 perror(PNAME
": fstat");
976 ctx
->datafile_dev
= st
.st_dev
;
977 ctx
->datafile_ino
= st
.st_ino
;
978 ctx
->straight_to_dump
= 0;
980 ctx
->datafile_dev
= -1;
981 ctx
->datafile_ino
= -1;
982 ctx
->straight_to_dump
= 1;
985 if (mode
== SCAN
|| mode
== SCANDUMP
) {
986 if (stat(scandir
, &st
) < 0) {
987 fprintf(stderr
, "%s: %s: stat: %s\n", PNAME
, scandir
,
991 ctx
->filesystem_dev
= crossfs ?
0 : st
.st_dev
;
996 ctx
->crossfs
= crossfs
;
997 ctx
->fakeatimes
= fakediratimes
;
998 ctx
->usemtime
= mtime
;
1000 ctx
->last_output_update
= time(NULL
);
1002 /* progress==1 means report progress only if stderr is a tty */
1004 progress
= isatty(2) ?
2 : 0;
1005 ctx
->progress
= progress
;
1009 ioctl(2, TIOCGWINSZ
, &ws
) == 0 &&
1011 ctx
->progwidth
= ws
.ws_col
- 1;
1013 ctx
->progwidth
= 79;
1016 if (mode
== SCANDUMP
)
1017 printf(DUMPHDR
"%02x\n", (unsigned char)pathsep
);
1020 * Scan the directory tree, and write out the trie component
1023 if (mode
!= SCANDUMP
) {
1024 ctx
->tb
= triebuild_new(fd
);
1029 while ((buf
= fgetline(stdin
)) != NULL
) {
1030 struct trie_file tf
;
1033 buf
[strcspn(buf
, "\r\n")] = '\0';
1037 while (*p
&& *p
!= ' ') p
++;
1039 fprintf(stderr
, "%s: dump file line %d: expected at least"
1040 " three fields\n", PNAME
, line
);
1044 tf
.size
= strtoull(q
, NULL
, 10);
1046 while (*p
&& *p
!= ' ') p
++;
1048 fprintf(stderr
, "%s: dump file line %d: expected at least"
1049 " three fields\n", PNAME
, line
);
1053 tf
.atime
= strtoull(q
, NULL
, 10);
1061 for (i
= 0; i
< 2; i
++) {
1063 if (*p
>= '0' && *p
<= '9')
1065 else if (*p
>= 'A' && *p
<= 'F')
1066 c
+= *p
- ('A' - 10);
1067 else if (*p
>= 'a' && *p
<= 'f')
1068 c
+= *p
- ('a' - 10);
1070 fprintf(stderr
, "%s: dump file line %d: unable"
1071 " to parse hex escape\n", PNAME
, line
);
1081 triebuild_add(ctx
->tb
, buf
, &tf
);
1086 du(scandir
, gotdata
, scan_error
, ctx
);
1088 if (mode
!= SCANDUMP
) {
1091 char *buf
, *prevbuf
;
1093 count
= triebuild_finish(ctx
->tb
);
1094 triebuild_free(ctx
->tb
);
1096 if (ctx
->progress
) {
1097 fprintf(stderr
, "%-*s\r", ctx
->progwidth
, "");
1102 * Work out how much space the cumulative index trees
1103 * will take; enlarge the file, and memory-map it.
1105 if (fstat(fd
, &st
) < 0) {
1106 perror(PNAME
": fstat");
1110 printf("Built pathname index, %d entries,"
1111 " %llu bytes of index\n", count
,
1112 (unsigned long long)st
.st_size
);
1114 totalsize
= index_initial_size(st
.st_size
, count
);
1115 totalsize
+= totalsize
/ 10;
1117 if (lseek(fd
, totalsize
-1, SEEK_SET
) < 0) {
1118 perror(PNAME
": lseek");
1121 if (write(fd
, "\0", 1) < 1) {
1122 perror(PNAME
": write");
1126 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
|PROT_WRITE
,MAP_SHARED
, fd
, 0);
1128 perror(PNAME
": mmap");
1132 if (fakediratimes
) {
1133 printf("Faking directory atimes\n");
1134 trie_fake_dir_atimes(mappedfile
);
1137 printf("Building index\n");
1138 ib
= indexbuild_new(mappedfile
, st
.st_size
, count
, &delta
);
1139 maxpathlen
= trie_maxpathlen(mappedfile
);
1140 buf
= snewn(maxpathlen
, char);
1141 prevbuf
= snewn(maxpathlen
, char);
1142 tw
= triewalk_new(mappedfile
);
1144 tf
= triewalk_next(tw
, buf
);
1146 prevtf
= NULL
; /* placate lint */
1150 if (totalsize
- indexbuild_realsize(ib
) < delta
) {
1151 const void *oldfile
= mappedfile
;
1155 * Unmap the file, grow it, and remap it.
1157 munmap(mappedfile
, totalsize
);
1160 totalsize
+= totalsize
/ 10;
1162 if (lseek(fd
, totalsize
-1, SEEK_SET
) < 0) {
1163 perror(PNAME
": lseek");
1166 if (write(fd
, "\0", 1) < 1) {
1167 perror(PNAME
": write");
1171 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
|PROT_WRITE
,MAP_SHARED
, fd
, 0);
1173 perror(PNAME
": mmap");
1177 indexbuild_rebase(ib
, mappedfile
);
1178 triewalk_rebase(tw
, mappedfile
);
1179 diff
= (const unsigned char *)mappedfile
-
1180 (const unsigned char *)oldfile
;
1182 prevtf
= (const struct trie_file
*)
1183 (((const unsigned char *)prevtf
) + diff
);
1185 tf
= (const struct trie_file
*)
1186 (((const unsigned char *)tf
) + diff
);
1190 * Get the next file from the index. So we are
1191 * currently holding, and have not yet
1192 * indexed, prevtf (with pathname prevbuf) and
1193 * tf (with pathname buf).
1196 memcpy(prevbuf
, buf
, maxpathlen
);
1197 tf
= triewalk_next(tw
, buf
);
1203 * Find the first differing character position
1204 * between our two pathnames.
1206 for (i
= 0; prevbuf
[i
] && prevbuf
[i
] == buf
[i
]; i
++);
1209 * If prevbuf was a directory name and buf is
1210 * something inside that directory, then
1211 * trie_before() will be called on prevbuf
1212 * itself. Hence we must drop a tag before it,
1213 * so that the resulting index is usable.
1215 if ((!prevbuf
[i
] && (buf
[i
] == pathsep
||
1216 (i
> 0 && buf
[i
-1] == pathsep
))))
1220 * Add prevtf to the index.
1222 indexbuild_add(ib
, prevtf
);
1226 * Drop an unconditional final tag, and
1227 * get out of this loop.
1234 * If prevbuf was a filename inside some
1235 * directory which buf is outside, then
1236 * trie_before() will be called on some
1237 * pathname either equal to buf or epsilon
1238 * less than it. Either way, we're going to
1239 * need to drop a tag after prevtf.
1241 if (strchr(prevbuf
+i
, pathsep
) || !tf
)
1246 realsize
= indexbuild_realsize(ib
);
1247 indexbuild_free(ib
);
1249 munmap(mappedfile
, totalsize
);
1250 ftruncate(fd
, realsize
);
1252 printf("Final index file size = %llu bytes\n",
1253 (unsigned long long)realsize
);
1255 } else if (mode
== TEXT
) {
1256 char *querydir
= actions
[action
].arg
;
1259 fd
= open(filename
, O_RDONLY
);
1261 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
1265 if (fstat(fd
, &st
) < 0) {
1266 perror(PNAME
": fstat");
1269 totalsize
= st
.st_size
;
1270 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
, MAP_SHARED
, fd
, 0);
1272 perror(PNAME
": mmap");
1275 pathsep
= trie_pathsep(mappedfile
);
1278 * Trim trailing slash, just in case.
1280 pathlen
= strlen(querydir
);
1281 if (pathlen
> 0 && querydir
[pathlen
-1] == pathsep
)
1282 querydir
[--pathlen
] = '\0';
1285 depth
= 1; /* default for text mode */
1286 if (outfile
!= NULL
) {
1287 FILE *fp
= fopen(outfile
, "w");
1289 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
,
1290 outfile
, strerror(errno
));
1293 text_query(mappedfile
, querydir
, textcutoff
, showfiles
,
1297 text_query(mappedfile
, querydir
, textcutoff
, showfiles
,
1301 munmap(mappedfile
, totalsize
);
1302 } else if (mode
== HTML
) {
1303 char *querydir
= actions
[action
].arg
;
1304 size_t pathlen
, maxpathlen
;
1306 struct html_config cfg
;
1310 fd
= open(filename
, O_RDONLY
);
1312 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
1315 printf("Status: 500\nContent-type: text/html\n\n"
1317 "<title>500 Internal Server Error</title>"
1319 "<h1>500 Internal Server Error</h1>"
1320 "<p><code>agedu</code> suffered an internal error."
1321 "</body></html>\n");
1326 if (fstat(fd
, &st
) < 0) {
1327 fprintf(stderr
, "%s: %s: fstat: %s\n", PNAME
, filename
,
1330 printf("Status: 500\nContent-type: text/html\n\n"
1332 "<title>500 Internal Server Error</title>"
1334 "<h1>500 Internal Server Error</h1>"
1335 "<p><code>agedu</code> suffered an internal error."
1336 "</body></html>\n");
1341 totalsize
= st
.st_size
;
1342 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
, MAP_SHARED
, fd
, 0);
1344 fprintf(stderr
, "%s: %s: mmap: %s\n", PNAME
, filename
,
1347 printf("Status: 500\nContent-type: text/html\n\n"
1349 "<title>500 Internal Server Error</title>"
1351 "<h1>500 Internal Server Error</h1>"
1352 "<p><code>agedu</code> suffered an internal error."
1353 "</body></html>\n");
1358 pathsep
= trie_pathsep(mappedfile
);
1360 maxpathlen
= trie_maxpathlen(mappedfile
);
1361 pathbuf
= snewn(maxpathlen
, char);
1363 if (!querydir
|| !gotdepth
) {
1365 * Single output file.
1368 cfg
.uriformat
= "/%|/%p/%|%|/%p";
1370 cfg
.uriformat
= NULL
;
1372 cfg
.autoage
= htmlautoagerange
;
1373 cfg
.oldest
= htmloldest
;
1374 cfg
.newest
= htmlnewest
;
1375 cfg
.showfiles
= showfiles
;
1377 cfg
.uriformat
= "/index.html%|/%/p.html";
1378 cfg
.fileformat
= "/index.html%|/%/p.html";
1379 cfg
.autoage
= htmlautoagerange
;
1380 cfg
.oldest
= htmloldest
;
1381 cfg
.newest
= htmlnewest
;
1382 cfg
.showfiles
= showfiles
;
1384 cfg
.html_title
= html_title
;
1388 * If we're run in --cgi mode, read PATH_INFO to get
1389 * a numeric pathname index.
1391 char *path_info
= getenv("PATH_INFO");
1399 if (!html_parse_path(mappedfile
, path_info
, &cfg
, &xi
)) {
1400 printf("Status: 404\nContent-type: text/html\n\n"
1402 "<title>404 Not Found</title>"
1404 "<h1>400 Not Found</h1>"
1405 "<p>Invalid <code>agedu</code> pathname."
1406 "</body></html>\n");
1411 * If the path was parseable but not canonically
1412 * expressed, return a redirect to the canonical
1415 char *canonpath
= html_format_path(mappedfile
, &cfg
, xi
);
1416 if (strcmp(canonpath
, path_info
)) {
1417 char *servername
= getenv("SERVER_NAME");
1418 char *scriptname
= getenv("SCRIPT_NAME");
1419 if (!servername
|| !scriptname
) {
1421 fprintf(stderr
, "%s: SCRIPT_NAME unset\n", PNAME
);
1422 else if (scriptname
)
1423 fprintf(stderr
, "%s: SCRIPT_NAME unset\n", PNAME
);
1425 fprintf(stderr
, "%s: SERVER_NAME and "
1426 "SCRIPT_NAME both unset\n", PNAME
);
1427 printf("Status: 500\nContent-type: text/html\n\n"
1429 "<title>500 Internal Server Error</title>"
1431 "<h1>500 Internal Server Error</h1>"
1432 "<p><code>agedu</code> suffered an internal "
1434 "</body></html>\n");
1437 printf("Status: 301\n"
1438 "Location: http://%s/%s%s\n"
1439 "Content-type: text/html\n\n"
1441 "<title>301 Moved</title>"
1443 "<h1>301 Moved</h1>"
1446 servername
, scriptname
, canonpath
);
1452 * In ordinary --html mode, process a query
1453 * directory passed in on the command line.
1457 * Trim trailing slash, just in case.
1459 pathlen
= strlen(querydir
);
1460 if (pathlen
> 0 && querydir
[pathlen
-1] == pathsep
)
1461 querydir
[--pathlen
] = '\0';
1463 xi
= trie_before(mappedfile
, querydir
);
1464 if (xi
>= trie_count(mappedfile
) ||
1465 (trie_getpath(mappedfile
, xi
, pathbuf
),
1466 strcmp(pathbuf
, querydir
))) {
1467 fprintf(stderr
, "%s: pathname '%s' does not exist in index\n"
1468 "%*s(check it is spelled exactly as it is in the "
1469 "index, including\n%*sany leading './')\n",
1471 (int)(1+sizeof(PNAME
)), "",
1472 (int)(1+sizeof(PNAME
)), "");
1474 } else if (!index_has_root(mappedfile
, xi
)) {
1475 fprintf(stderr
, "%s: pathname '%s' is"
1476 " a file, not a directory\n", PNAME
, querydir
);
1481 if (!querydir
|| !gotdepth
) {
1483 * Single output file.
1485 html
= html_query(mappedfile
, xi
, &cfg
, 1);
1486 if (querydir
&& outfile
!= NULL
) {
1487 FILE *fp
= fopen(outfile
, "w");
1489 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
,
1490 outfile
, strerror(errno
));
1492 } else if (fputs(html
, fp
) < 0) {
1493 fprintf(stderr
, "%s: %s: write: %s\n", PNAME
,
1494 outfile
, strerror(errno
));
1497 } else if (fclose(fp
) < 0) {
1498 fprintf(stderr
, "%s: %s: fclose: %s\n", PNAME
,
1499 outfile
, strerror(errno
));
1504 printf("Content-type: text/html\n\n");
1506 fputs(html
, stdout
);
1510 * Multiple output files.
1512 int dirlen
= outfile ?
2+strlen(outfile
) : 3;
1513 char prefix
[dirlen
];
1515 if (mkdir(outfile
, 0777) < 0 && errno
!= EEXIST
) {
1516 fprintf(stderr
, "%s: %s: mkdir: %s\n", PNAME
,
1517 outfile
, strerror(errno
));
1520 snprintf(prefix
, dirlen
, "%s/", outfile
);
1522 snprintf(prefix
, dirlen
, "./");
1526 * pathbuf is only set up in the plain-HTML case and
1527 * not in the CGI case; but that's OK, because the
1528 * CGI case can't come to this branch of the if
1531 make_successor(pathbuf
);
1532 xi2
= trie_before(mappedfile
, pathbuf
);
1534 if (html_dump(mappedfile
, xi
, xi2
, depth
, &cfg
, prefix
))
1538 munmap(mappedfile
, totalsize
);
1540 } else if (mode
== DUMP
) {
1544 fd
= open(filename
, O_RDONLY
);
1546 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
1550 if (fstat(fd
, &st
) < 0) {
1551 perror(PNAME
": fstat");
1554 totalsize
= st
.st_size
;
1555 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
, MAP_SHARED
, fd
, 0);
1557 perror(PNAME
": mmap");
1560 pathsep
= trie_pathsep(mappedfile
);
1562 maxpathlen
= trie_maxpathlen(mappedfile
);
1563 buf
= snewn(maxpathlen
, char);
1565 printf(DUMPHDR
"%02x\n", (unsigned char)pathsep
);
1566 tw
= triewalk_new(mappedfile
);
1567 while ((tf
= triewalk_next(tw
, buf
)) != NULL
)
1571 munmap(mappedfile
, totalsize
);
1572 } else if (mode
== HTTPD
) {
1573 struct html_config pcfg
;
1574 struct httpd_config dcfg
;
1576 fd
= open(filename
, O_RDONLY
);
1578 fprintf(stderr
, "%s: %s: open: %s\n", PNAME
, filename
,
1582 if (fstat(fd
, &st
) < 0) {
1583 perror(PNAME
": fstat");
1586 totalsize
= st
.st_size
;
1587 mappedfile
= mmap(NULL
, totalsize
, PROT_READ
, MAP_SHARED
, fd
, 0);
1589 perror(PNAME
": mmap");
1592 pathsep
= trie_pathsep(mappedfile
);
1594 dcfg
.address
= httpserveraddr
;
1595 dcfg
.port
= httpserverport
;
1596 dcfg
.closeoneof
= closeoneof
;
1597 dcfg
.basicauthdata
= httpauthdata
;
1598 pcfg
.uriformat
= "/%|/%p/%|%|/%p";
1599 pcfg
.autoage
= htmlautoagerange
;
1600 pcfg
.oldest
= htmloldest
;
1601 pcfg
.newest
= htmlnewest
;
1602 pcfg
.showfiles
= showfiles
;
1603 pcfg
.html_title
= html_title
;
1604 run_httpd(mappedfile
, auth
, &dcfg
, &pcfg
);
1605 munmap(mappedfile
, totalsize
);
1606 } else if (mode
== REMOVE
) {
1607 if (remove(filename
) < 0) {
1608 fprintf(stderr
, "%s: %s: remove: %s\n", PNAME
, filename
,