'configure' apparently doesn't bump the timestamp on config.h if it
[sgt/agedu] / html.c
1 /*
2 * html.c: implementation of html.h.
3 */
4
5 #include "agedu.h"
6 #include "html.h"
7 #include "alloc.h"
8 #include "trie.h"
9 #include "index.h"
10
11 #define MAXCOLOUR 511
12
13 struct html {
14 char *buf;
15 size_t buflen, bufsize;
16 const void *t;
17 unsigned long long totalsize, oldest, newest;
18 char *path2;
19 char *href;
20 size_t hreflen;
21 const char *format, *rootpage;
22 unsigned long long thresholds[MAXCOLOUR];
23 char *titletexts[MAXCOLOUR+1];
24 time_t now;
25 };
26
27 static void vhtprintf(struct html *ctx, const char *fmt, va_list ap)
28 {
29 va_list ap2;
30 int size, size2;
31 char testbuf[2];
32
33 va_copy(ap2, ap);
34 /*
35 * Some C libraries (Solaris, I'm looking at you) don't like
36 * an output buffer size of zero in vsnprintf, but will return
37 * sensible values given any non-zero buffer size. Hence, we
38 * use testbuf to gauge the length of the string.
39 */
40 size = vsnprintf(testbuf, 1, fmt, ap2);
41 va_end(ap2);
42
43 if (ctx->buflen + size >= ctx->bufsize) {
44 ctx->bufsize = (ctx->buflen + size) * 3 / 2 + 1024;
45 ctx->buf = sresize(ctx->buf, ctx->bufsize, char);
46 }
47 size2 = vsnprintf(ctx->buf + ctx->buflen, ctx->bufsize - ctx->buflen,
48 fmt, ap);
49 assert(size == size2);
50 ctx->buflen += size;
51 }
52
53 static void htprintf(struct html *ctx, const char *fmt, ...)
54 {
55 va_list ap;
56 va_start(ap, fmt);
57 vhtprintf(ctx, fmt, ap);
58 va_end(ap);
59 }
60
61 static unsigned long long round_and_format_age(struct html *ctx,
62 unsigned long long age,
63 char *buf, int direction)
64 {
65 struct tm tm, tm2;
66 char newbuf[80];
67 unsigned long long ret, newret;
68 int i;
69 int ym;
70 static const int minutes[] = { 5, 10, 15, 30, 45 };
71
72 tm = *localtime(&ctx->now);
73 ym = tm.tm_year * 12 + tm.tm_mon;
74
75 ret = ctx->now;
76 strcpy(buf, "Now");
77
78 for (i = 0; i < lenof(minutes); i++) {
79 newret = ctx->now - minutes[i] * 60;
80 sprintf(newbuf, "%d minutes", minutes[i]);
81 if (newret < age)
82 goto finish;
83 strcpy(buf, newbuf);
84 ret = newret;
85 }
86
87 for (i = 1; i < 24; i++) {
88 newret = ctx->now - i * (60*60);
89 sprintf(newbuf, "%d hour%s", i, i==1 ? "" : "s");
90 if (newret < age)
91 goto finish;
92 strcpy(buf, newbuf);
93 ret = newret;
94 }
95
96 for (i = 1; i < 7; i++) {
97 newret = ctx->now - i * (24*60*60);
98 sprintf(newbuf, "%d day%s", i, i==1 ? "" : "s");
99 if (newret < age)
100 goto finish;
101 strcpy(buf, newbuf);
102 ret = newret;
103 }
104
105 for (i = 1; i < 4; i++) {
106 newret = ctx->now - i * (7*24*60*60);
107 sprintf(newbuf, "%d week%s", i, i==1 ? "" : "s");
108 if (newret < age)
109 goto finish;
110 strcpy(buf, newbuf);
111 ret = newret;
112 }
113
114 for (i = 1; i < 11; i++) {
115 tm2 = tm; /* structure copy */
116 tm2.tm_year = (ym - i) / 12;
117 tm2.tm_mon = (ym - i) % 12;
118 newret = mktime(&tm2);
119 sprintf(newbuf, "%d month%s", i, i==1 ? "" : "s");
120 if (newret < age)
121 goto finish;
122 strcpy(buf, newbuf);
123 ret = newret;
124 }
125
126 for (i = 1;; i++) {
127 tm2 = tm; /* structure copy */
128 tm2.tm_year = (ym - i*12) / 12;
129 tm2.tm_mon = (ym - i*12) % 12;
130 newret = mktime(&tm2);
131 sprintf(newbuf, "%d year%s", i, i==1 ? "" : "s");
132 if (newret < age)
133 goto finish;
134 strcpy(buf, newbuf);
135 ret = newret;
136 }
137
138 finish:
139 if (direction > 0) {
140 /*
141 * Round toward newest, i.e. use the existing (buf,ret).
142 */
143 } else if (direction < 0) {
144 /*
145 * Round toward oldest, i.e. use (newbuf,newret);
146 */
147 strcpy(buf, newbuf);
148 ret = newret;
149 } else {
150 /*
151 * Round to nearest.
152 */
153 if (ret - age > age - newret) {
154 strcpy(buf, newbuf);
155 ret = newret;
156 }
157 }
158 return ret;
159 }
160
161 static void get_indices(const void *t, char *path,
162 unsigned long *xi1, unsigned long *xi2)
163 {
164 size_t pathlen = strlen(path);
165 int c1 = path[pathlen], c2 = (pathlen > 0 ? path[pathlen-1] : 0);
166
167 *xi1 = trie_before(t, path);
168 make_successor(path);
169 *xi2 = trie_before(t, path);
170 path[pathlen] = c1;
171 if (pathlen > 0)
172 path[pathlen-1] = c2;
173 }
174
175 static unsigned long long fetch_size(const void *t,
176 unsigned long xi1, unsigned long xi2,
177 unsigned long long atime)
178 {
179 if (xi2 - xi1 == 1) {
180 /*
181 * We are querying an individual file, so we should not
182 * depend on the index entries either side of the node,
183 * since they almost certainly don't both exist. Instead,
184 * just look up the file's size and atime in the main trie.
185 */
186 const struct trie_file *f = trie_getfile(t, xi1);
187 if (f->atime < atime)
188 return f->size;
189 else
190 return 0;
191 } else {
192 return index_query(t, xi2, atime) - index_query(t, xi1, atime);
193 }
194 }
195
196 static void htescape(struct html *ctx, const char *s, int n, int italics)
197 {
198 while (n > 0 && *s) {
199 unsigned char c = (unsigned char)*s++;
200
201 if (c == '&')
202 htprintf(ctx, "&amp;");
203 else if (c == '<')
204 htprintf(ctx, "&lt;");
205 else if (c == '>')
206 htprintf(ctx, "&gt;");
207 else if (c >= ' ' && c < '\177')
208 htprintf(ctx, "%c", c);
209 else {
210 if (italics) htprintf(ctx, "<i>");
211 htprintf(ctx, "[%02x]", c);
212 if (italics) htprintf(ctx, "</i>");
213 }
214
215 n--;
216 }
217 }
218
219 static void begin_colour_bar(struct html *ctx)
220 {
221 htprintf(ctx, "<table cellspacing=0 cellpadding=0"
222 " style=\"border:0\">\n<tr>\n");
223 }
224
225 static void add_to_colour_bar(struct html *ctx, int colour, int pixels)
226 {
227 int r, g, b;
228
229 if (colour >= 0 && colour < 256) /* red -> yellow fade */
230 r = 255, g = colour, b = 0;
231 else if (colour >= 256 && colour <= 511) /* yellow -> green fade */
232 r = 511 - colour, g = 255, b = 0;
233 else /* background grey */
234 r = g = b = 240;
235
236 if (pixels > 0) {
237 htprintf(ctx, "<td style=\"width:%dpx; height:1em; "
238 "background-color:#%02x%02x%02x\"",
239 pixels, r, g, b);
240 if (colour >= 0)
241 htprintf(ctx, " title=\"%s\"", ctx->titletexts[colour]);
242 htprintf(ctx, "></td>\n");
243 }
244 }
245
246 static void end_colour_bar(struct html *ctx)
247 {
248 htprintf(ctx, "</tr>\n</table>\n");
249 }
250
251 struct vector {
252 int want_href, essential;
253 char *name;
254 int literal; /* should the name be formatted in fixed-pitch? */
255 unsigned long index;
256 unsigned long long sizes[MAXCOLOUR+1];
257 };
258
259 int vec_compare(const void *av, const void *bv)
260 {
261 const struct vector *a = *(const struct vector **)av;
262 const struct vector *b = *(const struct vector **)bv;
263
264 if (a->sizes[MAXCOLOUR] > b->sizes[MAXCOLOUR])
265 return -1;
266 else if (a->sizes[MAXCOLOUR] < b->sizes[MAXCOLOUR])
267 return +1;
268 else if (a->want_href < b->want_href)
269 return +1;
270 else if (a->want_href > b->want_href)
271 return -1;
272 else if (a->want_href)
273 return strcmp(a->name, b->name);
274 else if (a->index < b->index)
275 return -1;
276 else if (a->index > b->index)
277 return +1;
278 else if (a->essential < b->essential)
279 return +1;
280 else if (a->essential > b->essential)
281 return -1;
282 return 0;
283 }
284
285 static struct vector *make_vector(struct html *ctx, char *path,
286 int want_href, int essential,
287 char *name, int literal)
288 {
289 unsigned long xi1, xi2;
290 struct vector *vec = snew(struct vector);
291 int i;
292
293 vec->want_href = want_href;
294 vec->essential = essential;
295 vec->name = name ? dupstr(name) : NULL;
296 vec->literal = literal;
297
298 get_indices(ctx->t, path, &xi1, &xi2);
299
300 vec->index = xi1;
301
302 for (i = 0; i <= MAXCOLOUR; i++) {
303 unsigned long long atime;
304 if (i == MAXCOLOUR)
305 atime = ULLONG_MAX;
306 else
307 atime = ctx->thresholds[i];
308 vec->sizes[i] = fetch_size(ctx->t, xi1, xi2, atime);
309 }
310
311 return vec;
312 }
313
314 static void print_heading(struct html *ctx, const char *title)
315 {
316 htprintf(ctx, "<tr style=\"padding: 0.2em; background-color:#e0e0e0\">\n"
317 "<td colspan=4 align=center>%s</td>\n</tr>\n", title);
318 }
319
320 static void compute_display_size(unsigned long long size,
321 const char **fmt, double *display_size)
322 {
323 static const char *const fmts[] = {
324 "%g b", "%g Kb", "%#.1f Mb", "%#.1f Gb", "%#.1f Tb",
325 "%#.1f Pb", "%#.1f Eb", "%#.1f Zb", "%#.1f Yb"
326 };
327 int shift = 0;
328 unsigned long long tmpsize;
329 double denominator;
330
331 tmpsize = size;
332 denominator = 1.0;
333 while (tmpsize >= 1024 && shift < lenof(fmts)-1) {
334 tmpsize >>= 10;
335 denominator *= 1024.0;
336 shift++;
337 }
338 *display_size = size / denominator;
339 *fmt = fmts[shift];
340 }
341
342 static void make_filename(char *buf, size_t buflen,
343 const char *format, const char *rootpage,
344 unsigned long index)
345 {
346 if (index == 0 && rootpage)
347 snprintf(buf, buflen, "%s", rootpage);
348 else
349 snprintf(buf, buflen, format, index);
350 }
351
352 #define PIXEL_SIZE 600 /* FIXME: configurability? */
353 static void write_report_line(struct html *ctx, struct vector *vec)
354 {
355 unsigned long long size, asize, divisor;
356 double display_size;
357 int pix, newpix;
358 int i;
359 const char *unitsfmt;
360
361 /*
362 * A line with literally zero space usage should not be
363 * printed at all if it's a link to a subdirectory (since it
364 * probably means the whole thing was excluded by some
365 * --exclude-path wildcard). If it's [files] or the top-level
366 * line, though, we must always print _something_, and in that
367 * case we must fiddle about to prevent divisions by zero in
368 * the code below.
369 */
370 if (!vec->sizes[MAXCOLOUR] && !vec->essential)
371 return;
372 divisor = ctx->totalsize;
373 if (!divisor) {
374 divisor = 1;
375 }
376
377 /*
378 * Find the total size of this subdirectory.
379 */
380 size = vec->sizes[MAXCOLOUR];
381 compute_display_size(size, &unitsfmt, &display_size);
382 htprintf(ctx, "<tr>\n"
383 "<td style=\"padding: 0.2em; text-align: right\">");
384 htprintf(ctx, unitsfmt, display_size);
385 htprintf(ctx, "</td>\n");
386
387 /*
388 * Generate a colour bar.
389 */
390 htprintf(ctx, "<td style=\"padding: 0.2em\">\n");
391 begin_colour_bar(ctx);
392 pix = 0;
393 for (i = 0; i <= MAXCOLOUR; i++) {
394 asize = vec->sizes[i];
395 newpix = asize * PIXEL_SIZE / divisor;
396 add_to_colour_bar(ctx, i, newpix - pix);
397 pix = newpix;
398 }
399 add_to_colour_bar(ctx, -1, PIXEL_SIZE - pix);
400 end_colour_bar(ctx);
401 htprintf(ctx, "</td>\n");
402
403 /*
404 * Output size as a percentage of totalsize.
405 */
406 htprintf(ctx, "<td style=\"padding: 0.2em; text-align: right\">"
407 "%.2f%%</td>\n", (double)size / divisor * 100.0);
408
409 /*
410 * Output a subdirectory marker.
411 */
412 htprintf(ctx, "<td style=\"padding: 0.2em\">");
413 if (vec->name) {
414 int doing_href = 0;
415
416 if (ctx->format && vec->want_href) {
417 make_filename(ctx->href, ctx->hreflen,
418 ctx->format, ctx->rootpage,
419 vec->index);
420 htprintf(ctx, "<a href=\"%s\">", ctx->href);
421 doing_href = 1;
422 }
423 if (vec->literal)
424 htprintf(ctx, "<code>");
425 htescape(ctx, vec->name, strlen(vec->name), 1);
426 if (vec->literal)
427 htprintf(ctx, "</code>");
428 if (doing_href)
429 htprintf(ctx, "</a>");
430 }
431 htprintf(ctx, "</td>\n</tr>\n");
432 }
433
434 int strcmptrailingpathsep(const char *a, const char *b)
435 {
436 while (*a == *b && *a)
437 a++, b++;
438
439 if ((*a == pathsep && !a[1] && !*b) ||
440 (*b == pathsep && !b[1] && !*a))
441 return 0;
442
443 return (int)(unsigned char)*a - (int)(unsigned char)*b;
444 }
445
446 char *html_query(const void *t, unsigned long index,
447 const struct html_config *cfg, int downlink)
448 {
449 struct html actx, *ctx = &actx;
450 char *path, *path2, *p, *q, *href;
451 char agebuf1[80], agebuf2[80];
452 size_t pathlen, subdirpos, hreflen;
453 unsigned long index2;
454 int i;
455 struct vector **vecs;
456 int nvecs, vecsize;
457 unsigned long xi1, xi2, xj1, xj2;
458
459 if (index >= trie_count(t))
460 return NULL;
461
462 ctx->buf = NULL;
463 ctx->buflen = ctx->bufsize = 0;
464 ctx->t = t;
465 ctx->format = cfg->format;
466 ctx->rootpage = cfg->rootpage;
467 htprintf(ctx, "<html>\n");
468
469 path = snewn(1+trie_maxpathlen(t), char);
470 ctx->path2 = path2 = snewn(1+trie_maxpathlen(t), char);
471 if (cfg->format) {
472 hreflen = strlen(cfg->format) + 100;
473 href = snewn(hreflen, char);
474 } else {
475 hreflen = 0;
476 href = NULL;
477 }
478 ctx->hreflen = hreflen;
479 ctx->href = href;
480
481 /*
482 * HEAD section.
483 */
484 htprintf(ctx, "<head>\n");
485 trie_getpath(t, index, path);
486 htprintf(ctx, "<title>%s: ", PNAME);
487 htescape(ctx, path, strlen(path), 0);
488 htprintf(ctx, "</title>\n");
489 htprintf(ctx, "</head>\n");
490
491 /*
492 * Begin BODY section.
493 */
494 htprintf(ctx, "<body>\n");
495 htprintf(ctx, "<h3 align=center>Disk space breakdown by"
496 " last-access time</h3>\n");
497
498 /*
499 * Show the pathname we're centred on, with hyperlinks to
500 * parent directories where available.
501 */
502 htprintf(ctx, "<p align=center>\n<code>");
503 q = path;
504 for (p = strchr(path, pathsep); p && p[1]; p = strchr(p, pathsep)) {
505 int doing_href = 0;
506 char c, *zp;
507
508 /*
509 * See if this path prefix exists in the trie. If so,
510 * generate a hyperlink.
511 */
512 zp = p;
513 if (p == path) /* special case for "/" at start */
514 zp++;
515
516 p++;
517
518 c = *zp;
519 *zp = '\0';
520 index2 = trie_before(t, path);
521 trie_getpath(t, index2, path2);
522 if (!strcmptrailingpathsep(path, path2) && cfg->format) {
523 make_filename(href, hreflen, cfg->format, cfg->rootpage, index2);
524 if (!*href) /* special case that we understand */
525 strcpy(href, "./");
526 htprintf(ctx, "<a href=\"%s\">", href);
527 doing_href = 1;
528 }
529 *zp = c;
530 htescape(ctx, q, zp - q, 1);
531 if (doing_href)
532 htprintf(ctx, "</a>");
533 htescape(ctx, zp, p - zp, 1);
534 q = p;
535 }
536 htescape(ctx, q, strlen(q), 1);
537 htprintf(ctx, "</code>\n");
538
539 /*
540 * Decide on the age limit of our colour coding, establish the
541 * colour thresholds, and write out a key.
542 */
543 ctx->now = time(NULL);
544 if (cfg->autoage) {
545 ctx->oldest = index_order_stat(t, 0.05);
546 ctx->newest = index_order_stat(t, 1.0);
547 ctx->oldest = round_and_format_age(ctx, ctx->oldest, agebuf1, -1);
548 ctx->newest = round_and_format_age(ctx, ctx->newest, agebuf2, +1);
549 } else {
550 ctx->oldest = cfg->oldest;
551 ctx->newest = cfg->newest;
552 ctx->oldest = round_and_format_age(ctx, ctx->oldest, agebuf1, 0);
553 ctx->newest = round_and_format_age(ctx, ctx->newest, agebuf2, 0);
554 }
555 for (i = 0; i < MAXCOLOUR; i++) {
556 ctx->thresholds[i] =
557 ctx->oldest + (ctx->newest - ctx->oldest) * i / (MAXCOLOUR-1);
558 }
559 for (i = 0; i <= MAXCOLOUR; i++) {
560 char buf[80];
561
562 if (i == 0) {
563 strcpy(buf, "&lt; ");
564 round_and_format_age(ctx, ctx->thresholds[0], buf+5, 0);
565 } else if (i == MAXCOLOUR) {
566 strcpy(buf, "&gt; ");
567 round_and_format_age(ctx, ctx->thresholds[MAXCOLOUR-1], buf+5, 0);
568 } else {
569 unsigned long long midrange =
570 (ctx->thresholds[i-1] + ctx->thresholds[i]) / 2;
571 round_and_format_age(ctx, midrange, buf, 0);
572 }
573
574 ctx->titletexts[i] = dupstr(buf);
575 }
576 htprintf(ctx, "<p align=center>Key to colour coding (mouse over for more detail):\n");
577 htprintf(ctx, "<p align=center style=\"padding: 0; margin-top:0.4em; "
578 "margin-bottom:1em\">");
579 begin_colour_bar(ctx);
580 htprintf(ctx, "<td style=\"padding-right:1em\">%s</td>\n", agebuf1);
581 for (i = 0; i < MAXCOLOUR; i++)
582 add_to_colour_bar(ctx, i, 1);
583 htprintf(ctx, "<td style=\"padding-left:1em\">%s</td>\n", agebuf2);
584 end_colour_bar(ctx);
585
586 /*
587 * Begin the main table.
588 */
589 htprintf(ctx, "<p align=center>\n<table style=\"margin:0; border:0\">\n");
590
591 /*
592 * Find the total size of our entire subdirectory. We'll use
593 * that as the scale for all the colour bars in this report.
594 */
595 get_indices(t, path, &xi1, &xi2);
596 ctx->totalsize = fetch_size(t, xi1, xi2, ULLONG_MAX);
597
598 /*
599 * Generate a report line for the whole subdirectory.
600 */
601 vecsize = 64;
602 vecs = snewn(vecsize, struct vector *);
603 nvecs = 1;
604 vecs[0] = make_vector(ctx, path, 0, 1, NULL, 0);
605 print_heading(ctx, "Overall");
606 write_report_line(ctx, vecs[0]);
607
608 /*
609 * Now generate report lines for all its children, and the
610 * files contained in it.
611 */
612 print_heading(ctx, "Subdirectories");
613
614 vecs[0]->name = dupstr("[files]");
615 get_indices(t, path, &xi1, &xi2);
616 xi1++;
617 pathlen = strlen(path);
618 subdirpos = pathlen + 1;
619 if (pathlen > 0 && path[pathlen-1] == pathsep)
620 subdirpos--;
621 while (xi1 < xi2) {
622 trie_getpath(t, xi1, path2);
623 get_indices(t, ctx->path2, &xj1, &xj2);
624 xi1 = xj2;
625 if (!cfg->showfiles && xj2 - xj1 <= 1)
626 continue; /* skip individual files */
627 if (nvecs >= vecsize) {
628 vecsize = nvecs * 3 / 2 + 64;
629 vecs = sresize(vecs, vecsize, struct vector *);
630 }
631 assert(strlen(path2) > pathlen);
632 vecs[nvecs] = make_vector(ctx, path2, downlink && (xj2 - xj1 > 1), 0,
633 path2 + subdirpos, 1);
634 for (i = 0; i <= MAXCOLOUR; i++)
635 vecs[0]->sizes[i] -= vecs[nvecs]->sizes[i];
636 nvecs++;
637 }
638
639 qsort(vecs, nvecs, sizeof(vecs[0]), vec_compare);
640
641 for (i = 0; i < nvecs; i++)
642 write_report_line(ctx, vecs[i]);
643
644 /*
645 * Close the main table.
646 */
647 htprintf(ctx, "</table>\n");
648
649 /*
650 * Finish up and tidy up.
651 */
652 htprintf(ctx, "</body>\n");
653 htprintf(ctx, "</html>\n");
654 sfree(href);
655 sfree(path2);
656 sfree(path);
657 for (i = 0; i < nvecs; i++) {
658 sfree(vecs[i]->name);
659 sfree(vecs[i]);
660 }
661 sfree(vecs);
662
663 return ctx->buf;
664 }
665
666 int html_dump(const void *t, unsigned long index, unsigned long endindex,
667 int maxdepth, const struct html_config *cfg,
668 const char *pathprefix)
669 {
670 /*
671 * Determine the filename for this file.
672 */
673 assert(cfg->format != NULL);
674 int prefixlen = strlen(pathprefix);
675 int fnmax = strlen(pathprefix) + strlen(cfg->format) + 100;
676 char filename[fnmax];
677 strcpy(filename, pathprefix);
678 make_filename(filename + prefixlen, fnmax - prefixlen,
679 cfg->format, cfg->rootpage, index);
680
681 /*
682 * Create the HTML itself. Don't write out downlinks from our
683 * deepest level.
684 */
685 char *html = html_query(t, index, cfg, maxdepth != 0);
686
687 /*
688 * Write it out.
689 */
690 FILE *fp = fopen(filename, "w");
691 if (!fp) {
692 fprintf(stderr, "%s: %s: open: %s\n", PNAME,
693 filename, strerror(errno));
694 return 1;
695 }
696 if (fputs(html, fp) < 0) {
697 fprintf(stderr, "%s: %s: write: %s\n", PNAME,
698 filename, strerror(errno));
699 fclose(fp);
700 return 1;
701 }
702 if (fclose(fp) < 0) {
703 fprintf(stderr, "%s: %s: fclose: %s\n", PNAME,
704 filename, strerror(errno));
705 return 1;
706 }
707
708 /*
709 * Recurse.
710 */
711 if (maxdepth != 0) {
712 unsigned long subindex, subendindex;
713 int newdepth = (maxdepth > 0 ? maxdepth - 1 : maxdepth);
714 char path[1+trie_maxpathlen(t)];
715
716 index++;
717 while (index < endindex) {
718 trie_getpath(t, index, path);
719 get_indices(t, path, &subindex, &subendindex);
720 index = subendindex;
721 if (subendindex - subindex > 1) {
722 if (html_dump(t, subindex, subendindex, newdepth,
723 cfg, pathprefix))
724 return 1;
725 }
726 }
727 }
728 return 0;
729 }