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 | |
23 | struct 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 | |
36 | static 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 | |
55 | static 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 | |
63 | static 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 | |
163 | static 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 | |
175 | static 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 | |
185 | static 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, "&"); |
192 | else if (c == '<') |
193 | htprintf(ctx, "<"); |
194 | else if (c == '>') |
195 | htprintf(ctx, ">"); |
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 | |
208 | static 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 | |
214 | static 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, "< "); |
230 | round_and_format_age(ctx, ctx->thresholds[0], buf+5, 0); |
231 | } else if (colour == MAXCOLOUR) { |
232 | strcpy(buf, "> "); |
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 | |
250 | static void end_colour_bar(struct html *ctx) |
251 | { |
252 | htprintf(ctx, "</tr>\n</table>\n"); |
253 | } |
254 | |
255 | struct vector { |
256 | int want_href; |
257 | char *name; |
258 | unsigned long index; |
259 | unsigned long long sizes[MAXCOLOUR+1]; |
260 | }; |
261 | |
262 | int 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 | |
284 | static 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 | |
310 | static 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? */ |
317 | static void write_report_line(struct html *ctx, struct vector *vec) |
318 | { |
319 | unsigned long long size, asize; |
320 | int pix, newpix; |
321 | int i; |
322 | |
323 | /* |
324 | * Find the total size of this subdirectory. |
325 | */ |
326 | size = vec->sizes[MAXCOLOUR]; |
327 | htprintf(ctx, "<tr>\n" |
328 | "<td style=\"padding: 0.2em; text-align: right\">%lluMb</td>\n", |
329 | ((size + ((1<<11)-1)) >> 11)); /* convert to Mb, rounding up */ |
330 | |
331 | /* |
332 | * Generate a colour bar. |
333 | */ |
334 | htprintf(ctx, "<td style=\"padding: 0.2em\">\n"); |
335 | begin_colour_bar(ctx); |
336 | pix = 0; |
337 | for (i = 0; i <= MAXCOLOUR; i++) { |
338 | asize = vec->sizes[i]; |
339 | newpix = asize * PIXEL_SIZE / ctx->totalsize; |
340 | add_to_colour_bar(ctx, i, newpix - pix); |
341 | pix = newpix; |
342 | } |
343 | add_to_colour_bar(ctx, -1, PIXEL_SIZE - pix); |
344 | end_colour_bar(ctx); |
345 | htprintf(ctx, "</td>\n"); |
346 | |
347 | /* |
348 | * Output size as a percentage of totalsize. |
349 | */ |
350 | htprintf(ctx, "<td style=\"padding: 0.2em; text-align: right\">" |
351 | "%.2f%%</td>\n", (double)size / ctx->totalsize * 100.0); |
352 | |
353 | /* |
354 | * Output a subdirectory marker. |
355 | */ |
356 | htprintf(ctx, "<td style=\"padding: 0.2em\">"); |
357 | if (vec->name) { |
358 | int doing_href = 0; |
359 | |
360 | if (ctx->format && vec->want_href) { |
361 | snprintf(ctx->href, ctx->hreflen, ctx->format, vec->index); |
362 | htprintf(ctx, "<a href=\"%s\">", ctx->href); |
363 | doing_href = 1; |
364 | } |
365 | htescape(ctx, vec->name, strlen(vec->name), 1); |
366 | if (doing_href) |
367 | htprintf(ctx, "</a>"); |
368 | } |
369 | htprintf(ctx, "</td>\n</tr>\n"); |
370 | } |
371 | |
372 | char *html_query(const void *t, unsigned long index, const char *format) |
373 | { |
374 | struct html actx, *ctx = &actx; |
375 | char *path, *path2, *p, *q, *href; |
376 | char agebuf1[80], agebuf2[80]; |
377 | size_t pathlen, hreflen; |
378 | unsigned long index2; |
379 | int i; |
380 | struct vector **vecs; |
381 | int nvecs, vecsize; |
382 | unsigned long xi1, xi2, xj1, xj2; |
383 | |
384 | if (index >= trie_count(t)) |
385 | return NULL; |
386 | |
387 | ctx->buf = NULL; |
388 | ctx->buflen = ctx->bufsize = 0; |
389 | ctx->t = t; |
390 | ctx->format = format; |
391 | htprintf(ctx, "<html>\n"); |
392 | |
393 | path = snewn(1+trie_maxpathlen(t), char); |
394 | ctx->path2 = path2 = snewn(1+trie_maxpathlen(t), char); |
395 | if (format) { |
396 | hreflen = strlen(format) + 100; |
397 | href = snewn(hreflen, char); |
398 | } else { |
399 | hreflen = 0; |
400 | href = NULL; |
401 | } |
402 | ctx->hreflen = hreflen; |
403 | ctx->href = href; |
404 | |
405 | /* |
406 | * HEAD section. |
407 | */ |
408 | htprintf(ctx, "<head>\n"); |
409 | trie_getpath(t, index, path); |
410 | htprintf(ctx, "<title>agedu: "); |
411 | htescape(ctx, path, strlen(path), 0); |
412 | htprintf(ctx, "</title>\n"); |
413 | htprintf(ctx, "</head>\n"); |
414 | |
415 | /* |
416 | * Begin BODY section. |
417 | */ |
418 | htprintf(ctx, "<body>\n"); |
419 | htprintf(ctx, "<h3 align=center>Disk space breakdown by" |
420 | " last-access time</h3>\n"); |
421 | |
422 | /* |
423 | * Show the pathname we're centred on, with hyperlinks to |
424 | * parent directories where available. |
425 | */ |
426 | htprintf(ctx, "<p align=center>\n<code>"); |
427 | q = path; |
428 | for (p = strchr(path, '/'); p; p = strchr(p+1, '/')) { |
429 | int doing_href = 0; |
430 | /* |
431 | * See if this path prefix exists in the trie. If so, |
432 | * generate a hyperlink. |
433 | */ |
434 | *p = '\0'; |
435 | index2 = trie_before(t, path); |
436 | trie_getpath(t, index2, path2); |
437 | if (!strcmp(path, path2) && format) { |
438 | snprintf(href, hreflen, format, index2); |
439 | htprintf(ctx, "<a href=\"%s\">", href); |
440 | doing_href = 1; |
441 | } |
442 | *p = '/'; |
443 | htescape(ctx, q, p - q, 1); |
444 | q = p + 1; |
445 | if (doing_href) |
446 | htprintf(ctx, "</a>"); |
447 | htprintf(ctx, "/"); |
448 | } |
449 | htescape(ctx, q, strlen(q), 1); |
450 | htprintf(ctx, "</code>\n"); |
451 | |
452 | /* |
453 | * Decide on the age limit of our colour coding, establish the |
454 | * colour thresholds, and write out a key. |
455 | */ |
456 | ctx->oldest = index_order_stat(t, 0.05); /* FIXME: configurability? */ |
457 | ctx->newest = index_order_stat(t, 1.0); |
458 | ctx->now = time(NULL); |
459 | ctx->oldest = round_and_format_age(ctx, ctx->oldest, agebuf1, -1); |
460 | ctx->newest = round_and_format_age(ctx, ctx->newest, agebuf2, +1); |
461 | for (i = 0; i < MAXCOLOUR-1; i++) { |
462 | ctx->thresholds[i] = |
463 | ctx->oldest + (ctx->newest - ctx->oldest) * i / MAXCOLOUR; |
464 | } |
465 | htprintf(ctx, "<p align=center>Key to colour coding (mouse over for more detail):\n"); |
466 | htprintf(ctx, "<p align=center style=\"padding: 0; margin-top:0.4em; " |
467 | "margin-bottom:1em\""); |
468 | begin_colour_bar(ctx); |
469 | htprintf(ctx, "<td style=\"padding-right:1em\">%s</td>\n", agebuf1); |
470 | for (i = 0; i < MAXCOLOUR; i++) |
471 | add_to_colour_bar(ctx, i, 1); |
472 | htprintf(ctx, "<td style=\"padding-left:1em\">%s</td>\n", agebuf2); |
473 | end_colour_bar(ctx); |
474 | |
475 | /* |
476 | * Begin the main table. |
477 | */ |
478 | htprintf(ctx, "<p align=center>\n<table style=\"margin:0; border:0\">\n"); |
479 | |
480 | /* |
481 | * Find the total size of our entire subdirectory. We'll use |
482 | * that as the scale for all the colour bars in this report. |
483 | */ |
484 | ctx->totalsize = fetch_size(t, path, ULLONG_MAX); |
485 | |
486 | /* |
487 | * Generate a report line for the whole subdirectory. |
488 | */ |
489 | vecsize = 64; |
490 | vecs = snewn(vecsize, struct vector *); |
491 | nvecs = 1; |
492 | vecs[0] = make_vector(ctx, path, 0, NULL); |
493 | print_heading(ctx, "Overall"); |
494 | write_report_line(ctx, vecs[0]); |
495 | |
496 | /* |
497 | * Now generate report lines for all its children, and the |
498 | * files contained in it. |
499 | */ |
500 | print_heading(ctx, "Subdirectories"); |
501 | |
502 | vecs[0]->name = dupstr("[files]"); |
503 | get_indices(t, path, &xi1, &xi2); |
504 | xi1++; |
505 | pathlen = strlen(path); |
506 | while (xi1 < xi2) { |
507 | trie_getpath(t, xi1, path2); |
508 | get_indices(t, ctx->path2, &xj1, &xj2); |
509 | xi1 = xj2; |
510 | if (xj2 - xj1 <= 1) |
511 | continue; /* skip individual files */ |
512 | if (nvecs >= vecsize) { |
513 | vecsize = nvecs * 3 / 2 + 64; |
514 | vecs = sresize(vecs, vecsize, struct vector *); |
515 | } |
516 | assert(strlen(path2) > pathlen); |
517 | vecs[nvecs] = make_vector(ctx, path2, 1, path2 + pathlen + 1); |
518 | for (i = 0; i <= MAXCOLOUR; i++) |
519 | vecs[0]->sizes[i] -= vecs[nvecs]->sizes[i]; |
520 | nvecs++; |
521 | } |
522 | |
523 | qsort(vecs, nvecs, sizeof(vecs[0]), vec_compare); |
524 | |
525 | for (i = 0; i < nvecs; i++) |
526 | write_report_line(ctx, vecs[i]); |
527 | |
528 | /* |
529 | * Close the main table. |
530 | */ |
531 | htprintf(ctx, "</table>\n"); |
532 | |
533 | /* |
534 | * Finish up and tidy up. |
535 | */ |
536 | htprintf(ctx, "</body>\n"); |
537 | htprintf(ctx, "</html>\n"); |
538 | sfree(href); |
539 | sfree(path2); |
540 | sfree(path); |
541 | for (i = 0; i < nvecs; i++) { |
542 | sfree(vecs[i]->name); |
543 | sfree(vecs[i]); |
544 | } |
545 | sfree(vecs); |
546 | |
547 | return ctx->buf; |
548 | } |