c1955b4ffc82f23c92f5e117243aa46b62ffbf83
[sgt/agedu] / agedu.c
1 /*
2 * Main program for agedu.
3 */
4
5 #define _GNU_SOURCE
6 #include <stdio.h>
7 #include <errno.h>
8 #include <stdarg.h>
9 #include <stdlib.h>
10 #include <stdint.h>
11 #include <string.h>
12 #include <time.h>
13
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <fcntl.h>
17 #include <sys/mman.h>
18 #include <termios.h>
19 #include <sys/ioctl.h>
20
21 #include "du.h"
22 #include "trie.h"
23 #include "index.h"
24 #include "malloc.h"
25 #include "html.h"
26 #include "httpd.h"
27
28 #define PNAME "agedu"
29
30 void fatal(const char *fmt, ...)
31 {
32 va_list ap;
33 fprintf(stderr, "%s: ", PNAME);
34 va_start(ap, fmt);
35 vfprintf(stderr, fmt, ap);
36 va_end(ap);
37 fprintf(stderr, "\n");
38 exit(1);
39 }
40
41 struct ctx {
42 triebuild *tb;
43 dev_t datafile_dev, filesystem_dev;
44 ino_t datafile_ino;
45 time_t last_output_update;
46 int progress, progwidth;
47 };
48
49 static int gotdata(void *vctx, const char *pathname, const struct stat64 *st)
50 {
51 struct ctx *ctx = (struct ctx *)vctx;
52 struct trie_file file;
53 time_t t;
54
55 /*
56 * Filter out our own data file.
57 */
58 if (st->st_dev == ctx->datafile_dev && st->st_ino == ctx->datafile_ino)
59 return 0;
60
61 /*
62 * Don't cross the streams^W^Wany file system boundary.
63 * (FIXME: this should be a configurable option.)
64 */
65 if (st->st_dev != ctx->filesystem_dev)
66 return 0;
67
68 /*
69 * FIXME: other filtering in gotdata will be needed, when we
70 * implement serious filtering.
71 */
72
73 file.blocks = st->st_blocks;
74 file.atime = st->st_atime;
75 triebuild_add(ctx->tb, pathname, &file);
76
77 t = time(NULL);
78 if (t != ctx->last_output_update) {
79 if (ctx->progress) {
80 fprintf(stderr, "%-*.*s\r", ctx->progwidth, ctx->progwidth,
81 pathname);
82 fflush(stderr);
83 }
84 ctx->last_output_update = t;
85 }
86
87 return 1;
88 }
89
90 static void run_query(const void *mappedfile, const char *rootdir,
91 time_t t, int depth)
92 {
93 size_t maxpathlen;
94 char *pathbuf;
95 unsigned long xi1, xi2;
96 unsigned long long s1, s2;
97
98 maxpathlen = trie_maxpathlen(mappedfile);
99 pathbuf = snewn(maxpathlen + 1, char);
100
101 /*
102 * We want to query everything between the supplied filename
103 * (inclusive) and that filename with a ^A on the end
104 * (exclusive). So find the x indices for each.
105 */
106 sprintf(pathbuf, "%s\001", rootdir);
107 xi1 = trie_before(mappedfile, rootdir);
108 xi2 = trie_before(mappedfile, pathbuf);
109
110 /*
111 * Now do the lookups in the age index.
112 */
113 s1 = index_query(mappedfile, xi1, t);
114 s2 = index_query(mappedfile, xi2, t);
115
116 /* Display in units of 2 512-byte blocks = 1Kb */
117 printf("%-11llu %s\n", (s2 - s1) / 2, rootdir);
118
119 if (depth > 0) {
120 /*
121 * Now scan for first-level subdirectories and report
122 * those too.
123 */
124 xi1++;
125 while (xi1 < xi2) {
126 trie_getpath(mappedfile, xi1, pathbuf);
127 run_query(mappedfile, pathbuf, t, depth-1);
128 strcat(pathbuf, "\001");
129 xi1 = trie_before(mappedfile, pathbuf);
130 }
131 }
132 }
133
134 int main(int argc, char **argv)
135 {
136 int fd, count;
137 struct ctx actx, *ctx = &actx;
138 struct stat st;
139 off_t totalsize, realsize;
140 void *mappedfile;
141 triewalk *tw;
142 indexbuild *ib;
143 const struct trie_file *tf;
144 char *filename = "agedu.dat";
145 char *rootdir = NULL;
146 int doing_opts = 1;
147 enum { QUERY, HTML, SCAN, DUMP, HTTPD } mode = QUERY;
148 char *minage = "0d";
149 int auth = HTTPD_AUTH_MAGIC | HTTPD_AUTH_BASIC;
150 int progress = 1;
151
152 while (--argc > 0) {
153 char *p = *++argv;
154 char *optval;
155
156 if (doing_opts && *p == '-') {
157 if (!strcmp(p, "--")) {
158 doing_opts = 0;
159 } else if (p[1] == '-') {
160 char *optval = strchr(p, '=');
161 if (optval)
162 *optval++ = '\0';
163 if (!strcmp(p, "--help")) {
164 printf("FIXME: usage();\n");
165 return 0;
166 } else if (!strcmp(p, "--version")) {
167 printf("FIXME: version();\n");
168 return 0;
169 } else if (!strcmp(p, "--licence") ||
170 !strcmp(p, "--license")) {
171 printf("FIXME: licence();\n");
172 return 0;
173 } else if (!strcmp(p, "--scan")) {
174 mode = SCAN;
175 } else if (!strcmp(p, "--dump")) {
176 mode = DUMP;
177 } else if (!strcmp(p, "--html")) {
178 mode = HTML;
179 } else if (!strcmp(p, "--httpd") ||
180 !strcmp(p, "--server")) {
181 mode = HTTPD;
182 } else if (!strcmp(p, "--progress") ||
183 !strcmp(p, "--scan-progress")) {
184 progress = 2;
185 } else if (!strcmp(p, "--no-progress") ||
186 !strcmp(p, "--no-scan-progress")) {
187 progress = 0;
188 } else if (!strcmp(p, "--tty-progress") ||
189 !strcmp(p, "--tty-scan-progress") ||
190 !strcmp(p, "--progress-tty") ||
191 !strcmp(p, "--scan-progress-tty")) {
192 progress = 1;
193 } else if (!strcmp(p, "--file") ||
194 !strcmp(p, "--auth") ||
195 !strcmp(p, "--http-auth") ||
196 !strcmp(p, "--httpd-auth") ||
197 !strcmp(p, "--server-auth") ||
198 !strcmp(p, "--minimum-age") ||
199 !strcmp(p, "--min-age") ||
200 !strcmp(p, "--age")) {
201 /*
202 * Long options requiring values.
203 */
204 if (!optval) {
205 if (--argc > 0) {
206 optval = *++argv;
207 } else {
208 fprintf(stderr, "%s: option '%s' requires"
209 " an argument\n", PNAME, p);
210 return 1;
211 }
212 }
213 if (!strcmp(p, "--file")) {
214 filename = optval;
215 } else if (!strcmp(p, "--minimum-age") ||
216 !strcmp(p, "--min-age") ||
217 !strcmp(p, "--age")) {
218 minage = optval;
219 } else if (!strcmp(p, "--auth") ||
220 !strcmp(p, "--http-auth") ||
221 !strcmp(p, "--httpd-auth") ||
222 !strcmp(p, "--server-auth")) {
223 if (!strcmp(optval, "magic"))
224 auth = HTTPD_AUTH_MAGIC;
225 else if (!strcmp(optval, "basic"))
226 auth = HTTPD_AUTH_BASIC;
227 else if (!strcmp(optval, "none"))
228 auth = HTTPD_AUTH_NONE;
229 else if (!strcmp(optval, "default"))
230 auth = HTTPD_AUTH_MAGIC | HTTPD_AUTH_BASIC;
231 else {
232 fprintf(stderr, "%s: unrecognised authentication"
233 " type '%s'\n%*s options are 'magic',"
234 " 'basic', 'none', 'default'\n",
235 PNAME, optval, (int)strlen(PNAME), "");
236 return 1;
237 }
238 }
239 } else {
240 fprintf(stderr, "%s: unrecognised option '%s'\n",
241 PNAME, p);
242 return 1;
243 }
244 } else {
245 p++;
246 while (*p) {
247 char c = *p++;
248
249 switch (c) {
250 /* Options requiring arguments. */
251 case 'f':
252 case 'a':
253 if (*p) {
254 optval = p;
255 p += strlen(p);
256 } else if (--argc > 0) {
257 optval = *++argv;
258 } else {
259 fprintf(stderr, "%s: option '-%c' requires"
260 " an argument\n", PNAME, c);
261 return 1;
262 }
263 switch (c) {
264 case 'f': /* data file name */
265 filename = optval;
266 break;
267 case 'a': /* maximum age */
268 minage = optval;
269 break;
270 }
271 break;
272 case 's':
273 mode = SCAN;
274 break;
275 default:
276 fprintf(stderr, "%s: unrecognised option '-%c'\n",
277 PNAME, c);
278 return 1;
279 }
280 }
281 }
282 } else {
283 if (!rootdir) {
284 rootdir = p;
285 } else {
286 fprintf(stderr, "%s: unexpected argument '%s'\n", PNAME, p);
287 return 1;
288 }
289 }
290 }
291
292 if (!rootdir)
293 rootdir = ".";
294
295 if (mode == SCAN) {
296
297 fd = open(filename, O_RDWR | O_TRUNC | O_CREAT, S_IRWXU);
298 if (fd < 0) {
299 fprintf(stderr, "%s: %s: open: %s\n", PNAME, filename,
300 strerror(errno));
301 return 1;
302 }
303
304 if (stat(rootdir, &st) < 0) {
305 fprintf(stderr, "%s: %s: stat: %s\n", PNAME, rootdir,
306 strerror(errno));
307 return 1;
308 }
309 ctx->filesystem_dev = st.st_dev;
310
311 if (fstat(fd, &st) < 0) {
312 perror("agedu: fstat");
313 return 1;
314 }
315 ctx->datafile_dev = st.st_dev;
316 ctx->datafile_ino = st.st_ino;
317
318 ctx->last_output_update = time(NULL);
319
320 /* progress==1 means report progress only if stderr is a tty */
321 if (progress == 1)
322 progress = isatty(2) ? 2 : 0;
323 ctx->progress = progress;
324 {
325 struct winsize ws;
326 if (progress && ioctl(2, TIOCGWINSZ, &ws) == 0)
327 ctx->progwidth = ws.ws_col - 1;
328 else
329 ctx->progwidth = 79;
330 }
331
332 /*
333 * Scan the directory tree, and write out the trie component
334 * of the data file.
335 */
336 ctx->tb = triebuild_new(fd);
337 du(rootdir, gotdata, ctx);
338 count = triebuild_finish(ctx->tb);
339 triebuild_free(ctx->tb);
340
341 if (ctx->progress) {
342 fprintf(stderr, "%-*s\r", ctx->progwidth, "");
343 fflush(stderr);
344 }
345
346 /*
347 * Work out how much space the cumulative index trees will
348 * take; enlarge the file, and memory-map it.
349 */
350 if (fstat(fd, &st) < 0) {
351 perror("agedu: fstat");
352 return 1;
353 }
354
355 printf("Built pathname index, %d entries, %ju bytes\n", count,
356 (intmax_t)st.st_size);
357
358 totalsize = index_compute_size(st.st_size, count);
359
360 if (lseek(fd, totalsize-1, SEEK_SET) < 0) {
361 perror("agedu: lseek");
362 return 1;
363 }
364 if (write(fd, "\0", 1) < 1) {
365 perror("agedu: write");
366 return 1;
367 }
368
369 printf("Upper bound on index file size = %ju bytes\n",
370 (intmax_t)totalsize);
371
372 mappedfile = mmap(NULL, totalsize, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
373 if (!mappedfile) {
374 perror("agedu: mmap");
375 return 1;
376 }
377
378 ib = indexbuild_new(mappedfile, st.st_size, count);
379 tw = triewalk_new(mappedfile);
380 while ((tf = triewalk_next(tw, NULL)) != NULL)
381 indexbuild_add(ib, tf);
382 triewalk_free(tw);
383 realsize = indexbuild_realsize(ib);
384 indexbuild_free(ib);
385
386 munmap(mappedfile, totalsize);
387 ftruncate(fd, realsize);
388 close(fd);
389 printf("Actual index file size = %ju bytes\n", (intmax_t)realsize);
390 } else if (mode == QUERY) {
391 time_t t;
392 struct tm tm;
393 int nunits;
394 char unit[2];
395 size_t pathlen;
396
397 t = time(NULL);
398
399 if (2 != sscanf(minage, "%d%1[DdWwMmYy]", &nunits, unit)) {
400 fprintf(stderr, "%s: minimum age should be a number followed by"
401 " one of d,w,m,y\n", PNAME);
402 return 1;
403 }
404
405 if (unit[0] == 'd') {
406 t -= 86400 * nunits;
407 } else if (unit[0] == 'w') {
408 t -= 86400 * 7 * nunits;
409 } else {
410 int ym;
411
412 tm = *localtime(&t);
413 ym = tm.tm_year * 12 + tm.tm_mon;
414
415 if (unit[0] == 'm')
416 ym -= nunits;
417 else
418 ym -= 12 * nunits;
419
420 tm.tm_year = ym / 12;
421 tm.tm_mon = ym % 12;
422
423 t = mktime(&tm);
424 }
425
426 fd = open(filename, O_RDONLY);
427 if (fd < 0) {
428 fprintf(stderr, "%s: %s: open: %s\n", PNAME, filename,
429 strerror(errno));
430 return 1;
431 }
432 if (fstat(fd, &st) < 0) {
433 perror("agedu: fstat");
434 return 1;
435 }
436 totalsize = st.st_size;
437 mappedfile = mmap(NULL, totalsize, PROT_READ, MAP_SHARED, fd, 0);
438 if (!mappedfile) {
439 perror("agedu: mmap");
440 return 1;
441 }
442
443 /*
444 * Trim trailing slash, just in case.
445 */
446 pathlen = strlen(rootdir);
447 if (pathlen > 0 && rootdir[pathlen-1] == '/')
448 rootdir[--pathlen] = '\0';
449
450 run_query(mappedfile, rootdir, t, 1);
451 } else if (mode == HTML) {
452 size_t pathlen;
453 unsigned long xi;
454 char *html;
455
456 fd = open(filename, O_RDONLY);
457 if (fd < 0) {
458 fprintf(stderr, "%s: %s: open: %s\n", PNAME, filename,
459 strerror(errno));
460 return 1;
461 }
462 if (fstat(fd, &st) < 0) {
463 perror("agedu: fstat");
464 return 1;
465 }
466 totalsize = st.st_size;
467 mappedfile = mmap(NULL, totalsize, PROT_READ, MAP_SHARED, fd, 0);
468 if (!mappedfile) {
469 perror("agedu: mmap");
470 return 1;
471 }
472
473 /*
474 * Trim trailing slash, just in case.
475 */
476 pathlen = strlen(rootdir);
477 if (pathlen > 0 && rootdir[pathlen-1] == '/')
478 rootdir[--pathlen] = '\0';
479
480 xi = trie_before(mappedfile, rootdir);
481 html = html_query(mappedfile, xi, NULL);
482 fputs(html, stdout);
483 } else if (mode == DUMP) {
484 size_t maxpathlen;
485 char *buf;
486
487 fd = open(filename, O_RDONLY);
488 if (fd < 0) {
489 fprintf(stderr, "%s: %s: open: %s\n", PNAME, filename,
490 strerror(errno));
491 return 1;
492 }
493 if (fstat(fd, &st) < 0) {
494 perror("agedu: fstat");
495 return 1;
496 }
497 totalsize = st.st_size;
498 mappedfile = mmap(NULL, totalsize, PROT_READ, MAP_SHARED, fd, 0);
499 if (!mappedfile) {
500 perror("agedu: mmap");
501 return 1;
502 }
503
504 maxpathlen = trie_maxpathlen(mappedfile);
505 buf = snewn(maxpathlen, char);
506
507 tw = triewalk_new(mappedfile);
508 while ((tf = triewalk_next(tw, buf)) != NULL) {
509 printf("%s: %llu %llu\n", buf, tf->blocks, tf->atime);
510 }
511 triewalk_free(tw);
512 } else if (mode == HTTPD) {
513 fd = open(filename, O_RDONLY);
514 if (fd < 0) {
515 fprintf(stderr, "%s: %s: open: %s\n", PNAME, filename,
516 strerror(errno));
517 return 1;
518 }
519 if (fstat(fd, &st) < 0) {
520 perror("agedu: fstat");
521 return 1;
522 }
523 totalsize = st.st_size;
524 mappedfile = mmap(NULL, totalsize, PROT_READ, MAP_SHARED, fd, 0);
525 if (!mappedfile) {
526 perror("agedu: mmap");
527 return 1;
528 }
529
530 run_httpd(mappedfile, auth);
531 }
532
533 return 0;
534 }