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