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