At Tom Womack's request, a trivial option to use mtimes instead of
[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;
22 unsigned long long thresholds[MAXCOLOUR-1];
23 time_t now;
24 };
25
26 static void vhtprintf(struct html *ctx, char *fmt, va_list ap)
27 {
28 va_list ap2;
29 int size, size2;
30 char testbuf[2];
31
32 va_copy(ap2, ap);
33 /*
34 * Some C libraries (Solaris, I'm looking at you) don't like
35 * an output buffer size of zero in vsnprintf, but will return
36 * sensible values given any non-zero buffer size. Hence, we
37 * use testbuf to gauge the length of the string.
38 */
39 size = vsnprintf(testbuf, 1, fmt, ap2);
40 va_end(ap2);
41
42 if (ctx->buflen + size >= ctx->bufsize) {
43 ctx->bufsize = (ctx->buflen + size) * 3 / 2 + 1024;
44 ctx->buf = sresize(ctx->buf, ctx->bufsize, char);
45 }
46 size2 = vsnprintf(ctx->buf + ctx->buflen, ctx->bufsize - ctx->buflen,
47 fmt, ap);
48 assert(size == size2);
49 ctx->buflen += size;
50 }
51
52 static void htprintf(struct html *ctx, char *fmt, ...)
53 {
54 va_list ap;
55 va_start(ap, fmt);
56 vhtprintf(ctx, fmt, ap);
57 va_end(ap);
58 }
59
60 static unsigned long long round_and_format_age(struct html *ctx,
61 unsigned long long age,
62 char *buf, int direction)
63 {
64 struct tm tm, tm2;
65 char newbuf[80];
66 unsigned long long ret, newret;
67 int i;
68 int ym;
69 static const int minutes[] = { 5, 10, 15, 30, 45 };
70
71 tm = *localtime(&ctx->now);
72 ym = tm.tm_year * 12 + tm.tm_mon;
73
74 ret = ctx->now;
75 strcpy(buf, "Now");
76
77 for (i = 0; i < lenof(minutes); i++) {
78 newret = ctx->now - minutes[i] * 60;
79 sprintf(newbuf, "%d minutes", minutes[i]);
80 if (newret < age)
81 goto finish;
82 strcpy(buf, newbuf);
83 ret = newret;
84 }
85
86 for (i = 1; i < 24; i++) {
87 newret = ctx->now - i * (60*60);
88 sprintf(newbuf, "%d hour%s", i, i==1 ? "" : "s");
89 if (newret < age)
90 goto finish;
91 strcpy(buf, newbuf);
92 ret = newret;
93 }
94
95 for (i = 1; i < 7; i++) {
96 newret = ctx->now - i * (24*60*60);
97 sprintf(newbuf, "%d day%s", i, i==1 ? "" : "s");
98 if (newret < age)
99 goto finish;
100 strcpy(buf, newbuf);
101 ret = newret;
102 }
103
104 for (i = 1; i < 4; i++) {
105 newret = ctx->now - i * (7*24*60*60);
106 sprintf(newbuf, "%d week%s", i, i==1 ? "" : "s");
107 if (newret < age)
108 goto finish;
109 strcpy(buf, newbuf);
110 ret = newret;
111 }
112
113 for (i = 1; i < 11; i++) {
114 tm2 = tm; /* structure copy */
115 tm2.tm_year = (ym - i) / 12;
116 tm2.tm_mon = (ym - i) % 12;
117 newret = mktime(&tm2);
118 sprintf(newbuf, "%d month%s", i, i==1 ? "" : "s");
119 if (newret < age)
120 goto finish;
121 strcpy(buf, newbuf);
122 ret = newret;
123 }
124
125 for (i = 1;; i++) {
126 tm2 = tm; /* structure copy */
127 tm2.tm_year = (ym - i*12) / 12;
128 tm2.tm_mon = (ym - i*12) % 12;
129 newret = mktime(&tm2);
130 sprintf(newbuf, "%d year%s", i, i==1 ? "" : "s");
131 if (newret < age)
132 goto finish;
133 strcpy(buf, newbuf);
134 ret = newret;
135 }
136
137 finish:
138 if (direction > 0) {
139 /*
140 * Round toward newest, i.e. use the existing (buf,ret).
141 */
142 } else if (direction < 0) {
143 /*
144 * Round toward oldest, i.e. use (newbuf,newret);
145 */
146 strcpy(buf, newbuf);
147 ret = newret;
148 } else {
149 /*
150 * Round to nearest.
151 */
152 if (ret - age > age - newret) {
153 strcpy(buf, newbuf);
154 ret = newret;
155 }
156 }
157 return ret;
158 }
159
160 static void get_indices(const void *t, char *path,
161 unsigned long *xi1, unsigned long *xi2)
162 {
163 size_t pathlen = strlen(path);
164 int c1 = path[pathlen], c2 = (pathlen > 0 ? path[pathlen-1] : 0);
165
166 *xi1 = trie_before(t, path);
167 make_successor(path);
168 *xi2 = trie_before(t, path);
169 path[pathlen] = c1;
170 if (pathlen > 0)
171 path[pathlen-1] = c2;
172 }
173
174 static unsigned long long fetch_size(const void *t, char *path,
175 unsigned long long atime)
176 {
177 unsigned long xi1, xi2;
178
179 get_indices(t, path, &xi1, &xi2);
180
181 return index_query(t, xi2, atime) - index_query(t, xi1, atime);
182 }
183
184 static void htescape(struct html *ctx, const char *s, int n, int italics)
185 {
186 while (n > 0 && *s) {
187 unsigned char c = (unsigned char)*s++;
188
189 if (c == '&')
190 htprintf(ctx, "&amp;");
191 else if (c == '<')
192 htprintf(ctx, "&lt;");
193 else if (c == '>')
194 htprintf(ctx, "&gt;");
195 else if (c >= ' ' && c < '\177')
196 htprintf(ctx, "%c", c);
197 else {
198 if (italics) htprintf(ctx, "<i>");
199 htprintf(ctx, "[%02x]", c);
200 if (italics) htprintf(ctx, "</i>");
201 }
202
203 n--;
204 }
205 }
206
207 static void begin_colour_bar(struct html *ctx)
208 {
209 htprintf(ctx, "<table cellspacing=0 cellpadding=0"
210 " style=\"border:0\">\n<tr>\n");
211 }
212
213 static void add_to_colour_bar(struct html *ctx, int colour, int pixels)
214 {
215 int r, g, b;
216 char buf[80];
217
218 if (colour >= 0 && colour < 256) /* red -> yellow fade */
219 r = 255, g = colour, b = 0;
220 else if (colour >= 256 && colour <= 511) /* yellow -> green fade */
221 r = 511 - colour, g = 255, b = 0;
222 else /* background grey */
223 r = g = b = 240;
224
225 if (colour < 0) {
226 /* no title text here */
227 } else if (colour == 0) {
228 strcpy(buf, "&lt; ");
229 round_and_format_age(ctx, ctx->thresholds[0], buf+5, 0);
230 } else if (colour == MAXCOLOUR) {
231 strcpy(buf, "&gt; ");
232 round_and_format_age(ctx, ctx->thresholds[MAXCOLOUR-1], buf+5, 0);
233 } else {
234 unsigned long long midrange =
235 (ctx->thresholds[colour] + ctx->thresholds[colour+1]) / 2;
236 round_and_format_age(ctx, midrange, buf, 0);
237 }
238
239 if (pixels > 0) {
240 htprintf(ctx, "<td style=\"width:%dpx; height:1em; "
241 "background-color:#%02x%02x%02x\"",
242 pixels, r, g, b);
243 if (colour >= 0)
244 htprintf(ctx, " title=\"%s\"", buf);
245 htprintf(ctx, "></td>\n");
246 }
247 }
248
249 static void end_colour_bar(struct html *ctx)
250 {
251 htprintf(ctx, "</tr>\n</table>\n");
252 }
253
254 struct vector {
255 int want_href;
256 char *name;
257 unsigned long index;
258 unsigned long long sizes[MAXCOLOUR+1];
259 };
260
261 int vec_compare(const void *av, const void *bv)
262 {
263 const struct vector *a = *(const struct vector **)av;
264 const struct vector *b = *(const struct vector **)bv;
265
266 if (a->sizes[MAXCOLOUR] > b->sizes[MAXCOLOUR])
267 return -1;
268 else if (a->sizes[MAXCOLOUR] < b->sizes[MAXCOLOUR])
269 return +1;
270 else if (a->want_href < b->want_href)
271 return +1;
272 else if (a->want_href > b->want_href)
273 return -1;
274 else if (a->want_href)
275 return strcmp(a->name, b->name);
276 else if (a->index < b->index)
277 return -1;
278 else if (a->index > b->index)
279 return +1;
280 return 0;
281 }
282
283 static struct vector *make_vector(struct html *ctx, char *path,
284 int want_href, char *name)
285 {
286 unsigned long xi1, xi2;
287 struct vector *vec = snew(struct vector);
288 int i;
289
290 vec->want_href = want_href;
291 vec->name = name ? dupstr(name) : NULL;
292
293 get_indices(ctx->t, path, &xi1, &xi2);
294
295 vec->index = xi1;
296
297 for (i = 0; i <= MAXCOLOUR; i++) {
298 unsigned long long atime;
299 if (i == MAXCOLOUR)
300 atime = ULLONG_MAX;
301 else
302 atime = ctx->thresholds[i];
303 vec->sizes[i] = fetch_size(ctx->t, path, atime);
304 }
305
306 return vec;
307 }
308
309 static void print_heading(struct html *ctx, const char *title)
310 {
311 htprintf(ctx, "<tr style=\"padding: 0.2em; background-color:#e0e0e0\">\n"
312 "<td colspan=4 align=center>%s</td>\n</tr>\n", title);
313 }
314
315 #define PIXEL_SIZE 600 /* FIXME: configurability? */
316 static void write_report_line(struct html *ctx, struct vector *vec)
317 {
318 unsigned long long size, asize, divisor;
319 int pix, newpix;
320 int i;
321
322 /*
323 * A line with literally zero space usage should not be
324 * printed at all if it's a link to a subdirectory (since it
325 * probably means the whole thing was excluded by some
326 * --exclude-path wildcard). If it's [files] or the top-level
327 * line, though, we must always print _something_, and in that
328 * case we must fiddle about to prevent divisions by zero in
329 * the code below.
330 */
331 if (!vec->sizes[MAXCOLOUR] && vec->want_href)
332 return;
333 divisor = ctx->totalsize;
334 if (!divisor) {
335 divisor = 1;
336 }
337
338 /*
339 * Find the total size of this subdirectory.
340 */
341 size = vec->sizes[MAXCOLOUR];
342 htprintf(ctx, "<tr>\n"
343 "<td style=\"padding: 0.2em; text-align: right\">%lluMb</td>\n",
344 ((size + ((1<<20)-1)) >> 20)); /* convert to Mb, rounding up */
345
346 /*
347 * Generate a colour bar.
348 */
349 htprintf(ctx, "<td style=\"padding: 0.2em\">\n");
350 begin_colour_bar(ctx);
351 pix = 0;
352 for (i = 0; i <= MAXCOLOUR; i++) {
353 asize = vec->sizes[i];
354 newpix = asize * PIXEL_SIZE / divisor;
355 add_to_colour_bar(ctx, i, newpix - pix);
356 pix = newpix;
357 }
358 add_to_colour_bar(ctx, -1, PIXEL_SIZE - pix);
359 end_colour_bar(ctx);
360 htprintf(ctx, "</td>\n");
361
362 /*
363 * Output size as a percentage of totalsize.
364 */
365 htprintf(ctx, "<td style=\"padding: 0.2em; text-align: right\">"
366 "%.2f%%</td>\n", (double)size / divisor * 100.0);
367
368 /*
369 * Output a subdirectory marker.
370 */
371 htprintf(ctx, "<td style=\"padding: 0.2em\">");
372 if (vec->name) {
373 int doing_href = 0;
374
375 if (ctx->format && vec->want_href) {
376 snprintf(ctx->href, ctx->hreflen, ctx->format, vec->index);
377 htprintf(ctx, "<a href=\"%s\">", ctx->href);
378 doing_href = 1;
379 }
380 htescape(ctx, vec->name, strlen(vec->name), 1);
381 if (doing_href)
382 htprintf(ctx, "</a>");
383 }
384 htprintf(ctx, "</td>\n</tr>\n");
385 }
386
387 char *html_query(const void *t, unsigned long index,
388 const struct html_config *cfg)
389 {
390 struct html actx, *ctx = &actx;
391 char *path, *path2, *p, *q, *href;
392 char agebuf1[80], agebuf2[80];
393 size_t pathlen, subdirpos, hreflen;
394 unsigned long index2;
395 int i;
396 struct vector **vecs;
397 int nvecs, vecsize;
398 unsigned long xi1, xi2, xj1, xj2;
399
400 if (index >= trie_count(t))
401 return NULL;
402
403 ctx->buf = NULL;
404 ctx->buflen = ctx->bufsize = 0;
405 ctx->t = t;
406 ctx->format = cfg->format;
407 htprintf(ctx, "<html>\n");
408
409 path = snewn(1+trie_maxpathlen(t), char);
410 ctx->path2 = path2 = snewn(1+trie_maxpathlen(t), char);
411 if (cfg->format) {
412 hreflen = strlen(cfg->format) + 100;
413 href = snewn(hreflen, char);
414 } else {
415 hreflen = 0;
416 href = NULL;
417 }
418 ctx->hreflen = hreflen;
419 ctx->href = href;
420
421 /*
422 * HEAD section.
423 */
424 htprintf(ctx, "<head>\n");
425 trie_getpath(t, index, path);
426 htprintf(ctx, "<title>%s: ", PNAME);
427 htescape(ctx, path, strlen(path), 0);
428 htprintf(ctx, "</title>\n");
429 htprintf(ctx, "</head>\n");
430
431 /*
432 * Begin BODY section.
433 */
434 htprintf(ctx, "<body>\n");
435 htprintf(ctx, "<h3 align=center>Disk space breakdown by"
436 " last-access time</h3>\n");
437
438 /*
439 * Show the pathname we're centred on, with hyperlinks to
440 * parent directories where available.
441 */
442 htprintf(ctx, "<p align=center>\n<code>");
443 q = path;
444 for (p = strchr(path, pathsep); p && p[1]; p = strchr(p, pathsep)) {
445 int doing_href = 0;
446 char c, *zp;
447
448 /*
449 * See if this path prefix exists in the trie. If so,
450 * generate a hyperlink.
451 */
452 zp = p;
453 if (p == path) /* special case for "/" at start */
454 zp++;
455
456 p++;
457
458 c = *zp;
459 *zp = '\0';
460 index2 = trie_before(t, path);
461 trie_getpath(t, index2, path2);
462 if (!strcmp(path, path2) && cfg->format) {
463 snprintf(href, hreflen, cfg->format, index2);
464 if (!*href) /* special case that we understand */
465 strcpy(href, "./");
466 htprintf(ctx, "<a href=\"%s\">", href);
467 doing_href = 1;
468 }
469 *zp = c;
470 htescape(ctx, q, zp - q, 1);
471 if (doing_href)
472 htprintf(ctx, "</a>");
473 htescape(ctx, zp, p - zp, 1);
474 q = p;
475 }
476 htescape(ctx, q, strlen(q), 1);
477 htprintf(ctx, "</code>\n");
478
479 /*
480 * Decide on the age limit of our colour coding, establish the
481 * colour thresholds, and write out a key.
482 */
483 ctx->now = time(NULL);
484 if (cfg->autoage) {
485 ctx->oldest = index_order_stat(t, 0.05);
486 ctx->newest = index_order_stat(t, 1.0);
487 ctx->oldest = round_and_format_age(ctx, ctx->oldest, agebuf1, -1);
488 ctx->newest = round_and_format_age(ctx, ctx->newest, agebuf2, +1);
489 } else {
490 ctx->oldest = cfg->oldest;
491 ctx->newest = cfg->newest;
492 ctx->oldest = round_and_format_age(ctx, ctx->oldest, agebuf1, 0);
493 ctx->newest = round_and_format_age(ctx, ctx->newest, agebuf2, 0);
494 }
495 for (i = 0; i < MAXCOLOUR-1; i++) {
496 ctx->thresholds[i] =
497 ctx->oldest + (ctx->newest - ctx->oldest) * i / MAXCOLOUR;
498 }
499 htprintf(ctx, "<p align=center>Key to colour coding (mouse over for more detail):\n");
500 htprintf(ctx, "<p align=center style=\"padding: 0; margin-top:0.4em; "
501 "margin-bottom:1em\"");
502 begin_colour_bar(ctx);
503 htprintf(ctx, "<td style=\"padding-right:1em\">%s</td>\n", agebuf1);
504 for (i = 0; i < MAXCOLOUR; i++)
505 add_to_colour_bar(ctx, i, 1);
506 htprintf(ctx, "<td style=\"padding-left:1em\">%s</td>\n", agebuf2);
507 end_colour_bar(ctx);
508
509 /*
510 * Begin the main table.
511 */
512 htprintf(ctx, "<p align=center>\n<table style=\"margin:0; border:0\">\n");
513
514 /*
515 * Find the total size of our entire subdirectory. We'll use
516 * that as the scale for all the colour bars in this report.
517 */
518 ctx->totalsize = fetch_size(t, path, ULLONG_MAX);
519
520 /*
521 * Generate a report line for the whole subdirectory.
522 */
523 vecsize = 64;
524 vecs = snewn(vecsize, struct vector *);
525 nvecs = 1;
526 vecs[0] = make_vector(ctx, path, 0, NULL);
527 print_heading(ctx, "Overall");
528 write_report_line(ctx, vecs[0]);
529
530 /*
531 * Now generate report lines for all its children, and the
532 * files contained in it.
533 */
534 print_heading(ctx, "Subdirectories");
535
536 vecs[0]->name = dupstr("[files]");
537 get_indices(t, path, &xi1, &xi2);
538 xi1++;
539 pathlen = strlen(path);
540 subdirpos = pathlen + 1;
541 if (pathlen > 0 && path[pathlen-1] == pathsep)
542 subdirpos--;
543 while (xi1 < xi2) {
544 trie_getpath(t, xi1, path2);
545 get_indices(t, ctx->path2, &xj1, &xj2);
546 xi1 = xj2;
547 if (xj2 - xj1 <= 1)
548 continue; /* skip individual files */
549 if (nvecs >= vecsize) {
550 vecsize = nvecs * 3 / 2 + 64;
551 vecs = sresize(vecs, vecsize, struct vector *);
552 }
553 assert(strlen(path2) > pathlen);
554 vecs[nvecs] = make_vector(ctx, path2, 1, path2 + subdirpos);
555 for (i = 0; i <= MAXCOLOUR; i++)
556 vecs[0]->sizes[i] -= vecs[nvecs]->sizes[i];
557 nvecs++;
558 }
559
560 qsort(vecs, nvecs, sizeof(vecs[0]), vec_compare);
561
562 for (i = 0; i < nvecs; i++)
563 write_report_line(ctx, vecs[i]);
564
565 /*
566 * Close the main table.
567 */
568 htprintf(ctx, "</table>\n");
569
570 /*
571 * Finish up and tidy up.
572 */
573 htprintf(ctx, "</body>\n");
574 htprintf(ctx, "</html>\n");
575 sfree(href);
576 sfree(path2);
577 sfree(path);
578 for (i = 0; i < nvecs; i++) {
579 sfree(vecs[i]->name);
580 sfree(vecs[i]);
581 }
582 sfree(vecs);
583
584 return ctx->buf;
585 }