ssnail

crappy and opinionated static site generator
git clone https://git.e1e0.net/ssnail.git
Log | Files | Refs | README | LICENSE

ssnail.c (16740B)


      1 /*
      2  * Copyright (c) 2020 Paco Esteban <paco@e1e0.net>
      3  *
      4  * Permission to use, copy, modify, and distribute this software for any
      5  * purpose with or without fee is hereby granted, provided that the above
      6  * copyright notice and this permission notice appear in all copies.
      7  *
      8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15 */
     16 
     17 #include <sys/queue.h>
     18 #include <sys/stat.h>
     19 #include <sys/syslimits.h>
     20 #include <sys/types.h>
     21 
     22 #include <assert.h>
     23 #include <dirent.h>
     24 #include <err.h>
     25 #include <errno.h>
     26 #include <stdio.h>
     27 #include <stdlib.h>
     28 #include <string.h>
     29 #include <time.h>
     30 #include <unistd.h>
     31 #include <lowdown.h>
     32 
     33 #include "helpers.h"
     34 #include "ssnail_error.h"
     35 
     36 #define VERSION "1.3"
     37 
     38 struct article {
     39 	size_t	    htmlz;
     40 	size_t	    origz;
     41 
     42 	char	   *src_path;
     43 	char	   *dst_path;
     44 	char	   *orig_content;
     45 	char	   *html_content;
     46 	char	   *title;
     47 	char	   *author;
     48 	char	   *date;
     49 	char	   *type;
     50 
     51 	long long   src_mtime;
     52 	long long   dst_mtime;
     53 
     54 	LIST_ENTRY(article) entries;
     55 };
     56 
     57 LIST_HEAD(listhead, article) head;
     58 
     59 __dead static void		  usage(void);
     60 static void			  free_article(struct article *);
     61 static void			  free_articleq(struct listhead *);
     62 static void			  add_index_entry(char **, struct article *);
     63 static void			  add_rss_entry(char **, struct article *, char *);
     64 
     65 struct article			 *init_article(void);
     66 static struct article		 *populate_article_entry(char *, char *);
     67 static struct article		 *generate_index(char *, char *, char *);
     68 
     69 static const struct ssnail_error *process_dir(char *, char *, int);
     70 static const struct ssnail_error *write_html(struct article *, char *, char *);
     71 static const struct ssnail_error *sort_articleq(struct listhead *);
     72 static const struct ssnail_error *gen_html(struct article *);
     73 static const struct ssnail_error *write_rss(char *, char *, char *, char *);
     74 
     75 __dead static void
     76 usage(void)
     77 {
     78 	fprintf(stderr, "usage: %s [-F] [-f footer] [-h header] [-i] [-r] [-v] "
     79 	    "-t <title> -u <url> src_folder dst_folder\n", getprogname());
     80 	exit(1);
     81 }
     82 
     83 int
     84 main(int argc, char *argv[])
     85 {
     86 	struct article			*ap = NULL, *ip = NULL;
     87 	const struct ssnail_error	*error = NULL;
     88 
     89 	int		 ch, index = 0, rss = 0, force = 0;
     90 	char		*header_tpl = NULL, *footer_tpl = NULL,
     91 			*index_listing = NULL, *rss_listing = NULL,
     92 			*title = NULL, *url = NULL;
     93 
     94 	header_tpl = strdup(HEADER);
     95 	if (header_tpl == NULL) {
     96 		error = ssnail_error_from_errno("header_tpl def");
     97 		goto done;
     98 	}
     99 	footer_tpl = strdup(FOOTER);
    100 	if (footer_tpl == NULL) {
    101 		error = ssnail_error_from_errno("footer_tpl def");
    102 		goto done;
    103 	}
    104 	index_listing = strdup("");
    105 	if (index_listing == NULL) {
    106 		error = ssnail_error_from_errno("index_listing def");
    107 		goto done;
    108 	}
    109 	rss_listing = strdup("");
    110 	if (rss_listing == NULL) {
    111 		error = ssnail_error_from_errno("rss_listing def");
    112 		goto done;
    113 	}
    114 
    115 	while ((ch = getopt(argc, argv, "Ff:h:irt:u:v")) != -1) {
    116 		switch (ch) {
    117 		case 'F':
    118 			force = 1;
    119 			break;
    120 		case 'f':
    121 			free(footer_tpl);
    122 			footer_tpl = strdup(optarg);
    123 			if (footer_tpl == NULL) {
    124 				error = ssnail_error_from_errno("footer_tpl");
    125 				goto done;
    126 			}
    127 			break;
    128 		case 'h':
    129 			free(header_tpl);
    130 			header_tpl = strdup(optarg);
    131 			if (header_tpl == NULL) {
    132 				error = ssnail_error_from_errno("footer_tpl");
    133 				goto done;
    134 			}
    135 			break;
    136 		case 'i':
    137 			index = 1;
    138 			break;
    139 		case 'r':
    140 			rss = 1;
    141 			break;
    142 		case 't':
    143 			title = strdup(optarg);
    144 			if (title == NULL) {
    145 				error = ssnail_error_from_errno("title");
    146 				goto done;
    147 			}
    148 			break;
    149 		case 'u':
    150 			url = strdup(optarg);
    151 			if (url == NULL) {
    152 				error = ssnail_error_from_errno("url");
    153 				goto done;
    154 			}
    155 			break;
    156 		case 'v':
    157 			printf("%s v%s\n", getprogname(), VERSION);
    158 			return EXIT_SUCCESS;
    159 		default:
    160 			usage();
    161 		}
    162 	}
    163 	argc -= optind;
    164 	argv += optind;
    165 
    166 	LIST_INIT(&head);  /* init the linked list */
    167 
    168 	/* title and usage are mandatory */
    169 	if (!title || !url)
    170 		usage();
    171 	/* src and dst dir are mandatory. */
    172 	if (argc < 2)
    173 		usage();
    174 
    175 	if ((error = process_dir(argv[0], argv[1], force)) != NULL)
    176 		goto done;
    177 
    178 	if ((error = sort_articleq(&head)) != NULL)
    179 		goto done;
    180 
    181 	printf("Generate html files ... \n");
    182 	LIST_FOREACH(ap, &head, entries) {
    183 		if ((ap->src_mtime > ap->dst_mtime) || force) {
    184 			printf("... %s\n", ap->dst_path);
    185 			error = write_html(ap, header_tpl, footer_tpl);
    186 			if (error)
    187 				goto done;
    188 			index = 1;
    189 			rss = 1;
    190 		}
    191 		if (strcmp(ap->type, "article") == 0) {
    192 			add_index_entry(&index_listing, ap);
    193 			add_rss_entry(&rss_listing, ap, url);
    194 		}
    195 	}
    196 
    197 	if (index || force) {
    198 		printf("Generate index ... \n");
    199 		ip = generate_index(argv[1], index_listing, title);
    200 		if (ip == NULL) {
    201 			error = ssnail_error_msg(2, "index");
    202 			goto done;
    203 		}
    204 		error = write_html(ip, header_tpl, footer_tpl);
    205 		if (error)
    206 			goto done;
    207 	}
    208 
    209 	if (rss || force) {
    210 		printf("Generate rss ... \n");
    211 		error = write_rss(argv[1], rss_listing, title, url);
    212 	}
    213 
    214 done:
    215 	free(title);
    216 	free(url);
    217 	free(header_tpl);
    218 	free(footer_tpl);
    219 	free(index_listing);
    220 	free(rss_listing);
    221 	free_article(ip);
    222 	free_articleq(&head);
    223 
    224 	if (error) {
    225 		ssnail_error_fprintf(error);
    226 		return error->code;
    227 	}
    228 
    229 	return EXIT_SUCCESS;
    230 }
    231 
    232 static void
    233 free_articleq(struct listhead *h)
    234 {
    235 	struct article	*a;
    236 
    237 	while (!LIST_EMPTY(h)) {
    238 		a = LIST_FIRST(h);
    239 		LIST_REMOVE(a, entries);
    240 		free_article(a);
    241 	}
    242 }
    243 
    244 static void
    245 free_article(struct article *a)
    246 {
    247 	if (a) {
    248 		free(a->src_path);
    249 		free(a->dst_path);
    250 		free(a->orig_content);
    251 		free(a->html_content);
    252 		free(a->title);
    253 		free(a->author);
    254 		free(a->date);
    255 		free(a->type);
    256 		free(a);
    257 	}
    258 }
    259 
    260 static const struct ssnail_error *
    261 gen_html(struct article *a)
    262 {
    263 	struct lowdown_opts		 opts;
    264 	struct lowdown_metaq	    	 mq;
    265 	struct lowdown_meta		*md;
    266 	const struct ssnail_error	*error = NULL;
    267 
    268 	TAILQ_INIT(&mq); /* create the queue(linked list) of lowdown_meta */
    269 	memset(&opts, 0, sizeof(struct lowdown_opts)); /* init opts to 0 */
    270 
    271 	opts.maxdepth = 128;
    272 	opts.type = LOWDOWN_HTML;
    273 	opts.feat = LOWDOWN_FOOTNOTES |
    274 		LOWDOWN_AUTOLINK |
    275 		LOWDOWN_TABLES |
    276 		LOWDOWN_SUPER |
    277 		LOWDOWN_STRIKE |
    278 		LOWDOWN_FENCED |
    279 		LOWDOWN_MATH |
    280 		LOWDOWN_METADATA;
    281 	opts.oflags = LOWDOWN_HTML_HEAD_IDS |
    282 		LOWDOWN_HTML_NUM_ENT |
    283 		LOWDOWN_HTML_OWASP |
    284 		LOWDOWN_HTML_SKIP_HTML;
    285 
    286 	/* use strlen(orig) here because the of the \0 char that
    287 	 * was showing up on the dest buffer.
    288 	 * playing with origz did not work here.
    289 	 */
    290 	lowdown_buf(&opts, a->orig_content, strlen(a->orig_content),
    291 	    &a->html_content, &a->htmlz, &mq);
    292 
    293 	/* setup default type if not set */
    294 	if ((a->type = strdup("page")) == NULL) {
    295 		error = ssnail_error_from_errno("def type");
    296 		goto out;
    297 	}
    298 
    299 	/* collect metadata from markdown */
    300 	TAILQ_FOREACH(md, &mq, entries) {
    301 		if (strcmp(md->key, "title") == 0 ) {
    302 			if ((a->title = strdup(md->value)) == NULL) {
    303 				error = ssnail_error_from_errno("title");
    304 				goto out;
    305 			}
    306 		}
    307 		if (strcmp(md->key, "author") == 0 ) {
    308 			if ((a->author = strdup(md->value)) == NULL) {
    309 				error = ssnail_error_from_errno("author");
    310 				goto out;
    311 			}
    312 		}
    313 		if (strcmp(md->key, "date") == 0 ) {
    314 			if ((a->date = strdup(md->value)) == NULL) {
    315 				error = ssnail_error_from_errno("date");
    316 				goto out;
    317 			}
    318 		}
    319 		if (strcmp(md->key, "type") == 0 ) {
    320 			free(a->type);
    321 			if ((a->type = strdup(md->value)) == NULL) {
    322 				error = ssnail_error_from_errno("type");
    323 				goto out;
    324 			}
    325 		}
    326 	}
    327 
    328 out:
    329 	lowdown_metaq_free(&mq);
    330 
    331 	return error;
    332 }
    333 
    334 static const struct ssnail_error *
    335 write_html(struct article *ap, char *head_tpl, char *foot_tpl)
    336 {
    337 	const struct ssnail_error	*error = NULL;
    338 	FILE				*fout;
    339 	char				*header = NULL, *footer = NULL;
    340 	int				 c = 0;
    341 
    342 	if (ap->title == NULL || ap->author == NULL || ap->date == NULL) {
    343 		error = ssnail_error_msg(2, "metadata");
    344 		goto out;
    345 	}
    346 
    347 	if (load_from_file(&header, head_tpl) == 0) {
    348 		error = ssnail_error_from_errno("load header");
    349 		goto out;
    350 	}
    351 	c += str_rep(&header, "$title$", ap->title);
    352 	c += str_rep(&header, "$author$", ap->author);
    353 	c += str_rep(&header, "$date$", ap->date);
    354 	if (c != 0) {
    355 		error = ssnail_error_from_errno("str_rep");
    356 		goto out;
    357 	}
    358 	if (load_from_file(&footer, foot_tpl) == 0) {
    359 		error = ssnail_error_from_errno("load footer");
    360 		goto out;
    361 	}
    362 	c = str_rep(&footer, "$title$", ap->title);
    363 	if (c != 0) {
    364 		error = ssnail_error_from_errno("str_rep");
    365 		goto out;
    366 	}
    367 
    368 	if ((fout = fopen(ap->dst_path, "w")) == NULL) {
    369 		error = ssnail_error_from_errno("openw dst_path");
    370 		goto out;
    371 	}
    372 	if (fwrite(header, 1, strlen(header), fout) == 0) {
    373 		error = ssnail_error_from_errno("fwrite header");
    374 		goto out;
    375 	}
    376 	if (fwrite(ap->html_content, 1, ap->htmlz, fout) == 0) {
    377 		error = ssnail_error_from_errno("fwrite html content");
    378 		goto out;
    379 	}
    380 	if (fwrite(footer, 1, strlen(footer), fout) == 0) {
    381 		error = ssnail_error_from_errno("fwrite footer");
    382 		goto out;
    383 	}
    384 	if (fclose(fout) != 0)
    385 		error = ssnail_error_from_errno("close fout");
    386 
    387 out:
    388 	free(header);
    389 	free(footer);
    390 	return error;
    391 }
    392 
    393 static struct article *
    394 populate_article_entry(char *src_path, char *dst_path)
    395 {
    396 	struct article		*ap = init_article();
    397 	struct stat		 st;
    398 	int			 c = 0;
    399 
    400 	if (ap == NULL)
    401 		return NULL;
    402 
    403 	ap->src_path = strndup(src_path, PATH_MAX);
    404 	if (ap->src_path == NULL)
    405 		goto error;
    406 
    407 	ap->dst_path = strndup(dst_path, PATH_MAX);
    408 	if (ap->dst_path == NULL)
    409 		goto error;
    410 	c = str_rep(&(ap->dst_path), ".md", ".html");
    411 	if (c == -1)
    412 		goto error;
    413 
    414 	if (stat(ap->src_path, &st) == -1)
    415 		goto error;
    416 	ap->src_mtime = st.st_mtim.tv_sec;
    417 
    418 	if (stat(ap->dst_path, &st) == -1) { /* dst file does not have to exist */
    419 		if (errno != ENOENT)
    420 			goto error;
    421 		ap->dst_mtime = 0;
    422 	} else {
    423 		ap->dst_mtime = st.st_mtim.tv_sec;
    424 	}
    425 
    426 	if ((ap->origz = load_from_file(&ap->orig_content, ap->src_path)) == 0)
    427 		goto error;
    428 
    429 	if (gen_html(ap) != 0)
    430 		goto error;
    431 
    432 	return ap;
    433 error:
    434 	free_article(ap);
    435 	return NULL;
    436 }
    437 
    438 static const struct ssnail_error *
    439 process_dir(char *src, char *dst, int force)
    440 {
    441 	DIR				*dirp;
    442 	struct dirent			*dp;
    443 	struct stat			 st;
    444 	struct article			*a = NULL;
    445 	const struct ssnail_error	*error = NULL;
    446 
    447 	char		*src_path = NULL, *dst_path = NULL;
    448 
    449 	/* if dst folder does not exist, create it */
    450 	if (stat(dst, &st) == -1 && errno == ENOENT) {
    451 		if (mkdir(dst, 0755) == -1)
    452 			return ssnail_error_from_errno("mkdir dst");
    453 	}
    454 
    455 	if ((dirp = opendir(src)) == NULL)
    456 		return ssnail_error_from_errno("opendir src");
    457 
    458 	while ((dp = readdir(dirp)) != NULL) {
    459 		if (strcmp(dp->d_name, ".") == 0
    460 		    || strcmp(dp->d_name, "..") == 0)
    461 			continue;
    462 
    463 		if ((src_path = build_full_path(src, dp->d_name)) == NULL) {
    464 			error = ssnail_error_msg(2, "buildpath src");
    465 			goto out;
    466 		}
    467 		if ((dst_path = build_full_path(dst, dp->d_name)) == NULL) {
    468 			error = ssnail_error_msg(2, "buildpath src");
    469 			goto out;
    470 		}
    471 
    472 		if (dp->d_type == DT_REG) {
    473 			if (strcmp(get_filename_ext(src_path), "md") == 0) {
    474 				a = populate_article_entry(src_path, dst_path);
    475 				if (a == NULL) {
    476 					error = ssnail_error_from_errno("populate");
    477 					goto out;
    478 				}
    479 				LIST_INSERT_HEAD(&head, a, entries);
    480 			} else {
    481 				error = copy_file(src_path, dst_path, force);
    482 				if (error)
    483 					goto out;
    484 			}
    485 		}
    486 		if (dp->d_type == DT_DIR)
    487 			error = process_dir(src_path, dst_path, force);
    488 
    489 		free(src_path);
    490 		free(dst_path);
    491 	}
    492 
    493 out:
    494 	closedir(dirp);
    495 	return error;
    496 }
    497 
    498 const struct ssnail_error *
    499 sort_articleq(struct listhead *h)
    500 {
    501 	struct article *a1 = NULL, *a2 = NULL;
    502 	int count_elems = 0;
    503 
    504 	assert(h != NULL);
    505 
    506 	if (LIST_EMPTY(h))
    507 		return ssnail_error_msg(2, "sort empty list");
    508 
    509 	/* count list elemets */
    510 	LIST_FOREACH(a1, h, entries) {
    511 		count_elems++;
    512 	}
    513 
    514 	for (int i = 0; i < count_elems; i++) {
    515 		a1 = LIST_FIRST(h);
    516 
    517 		/* move element to the "right" until is the smallest value */
    518 		/* or we're at the end of the list */
    519 		while ((a2 = LIST_NEXT(a1, entries)) != NULL) {
    520 			if (!a1->date || !a2->date)
    521 				return ssnail_error_msg(2, "cmp date NULL");
    522 			if (strcmp(a1->date, a2->date) < 0) {
    523 				LIST_REMOVE(a2, entries);
    524 				LIST_INSERT_BEFORE(a1, a2, entries);
    525 			} else
    526 				a1 = a2;
    527 		}
    528 	}
    529 
    530 	return NULL;
    531 }
    532 
    533 static struct article *
    534 generate_index(char *dst_path, char *index_listing, char *title) {
    535 	assert(index_listing != NULL);
    536 
    537 	struct article	    *a = init_article();
    538 	struct tm	    *timeinfo;
    539 	time_t		     now;
    540 
    541 	if ((a->dst_path = build_full_path(dst_path, "index.html")) == NULL)
    542 		goto error;
    543 
    544 	a->author = malloc(LOGIN_NAME_MAX);
    545 	if ((getlogin_r(a->author, LOGIN_NAME_MAX)) != 0)
    546 		goto error;
    547 
    548 	time(&now);
    549 	timeinfo = localtime(&now);
    550 	a->date = malloc(SHORT_DATE_Z);
    551 	if (strftime(a->date, SHORT_DATE_Z, "%Y-%m-%d", timeinfo) == 0)
    552 		goto error;
    553 
    554 	if ((a->title = strdup(title)) == NULL)
    555 		goto error;
    556 	if ((a->type = strdup("index")) == NULL)
    557 		goto error;
    558 	int ret = asprintf(&a->html_content, "<ul>\n%s</ul>\n", index_listing);
    559 	if (ret == -1)
    560 		goto error;
    561 	a->htmlz = strlen(a->html_content);
    562 
    563 	return a;
    564 
    565 error:
    566 	free_article(a);
    567 	return NULL;
    568 }
    569 
    570 static void
    571 add_index_entry(char **index_listing, struct article *a)
    572 {
    573 	char *tmp = NULL;
    574 
    575 	if (!index_listing || !a)
    576 		return;
    577 
    578 	const char *entry_format =
    579 	    "%s<li><a href=\"%s\" title=\"%s\">%s</a></li>\n";
    580 	char *href = strrchr(a->dst_path, '/');
    581 
    582 	if (href) {
    583 		/* keep the old listing address so we don't leak */
    584 		tmp = *index_listing;
    585 		asprintf(index_listing, entry_format, *index_listing,
    586 		    href, a->date, a->title);
    587 		free(tmp);
    588 	}
    589 }
    590 
    591 struct article *
    592 init_article(void)
    593 {
    594 	struct article *a = malloc(sizeof(struct article));
    595 
    596 	if (a) {
    597 		a->src_mtime = 0;
    598 		a->dst_mtime = 0;
    599 		a->htmlz = 0;
    600 		a->origz = 0;
    601 		a->src_path = NULL;
    602 		a->dst_path = NULL;
    603 		a->orig_content = NULL;
    604 		a->html_content = NULL;
    605 		a->title = NULL;
    606 		a->author = NULL;
    607 		a->date = NULL;
    608 		a->type = NULL;
    609 	}
    610 
    611 	return a;
    612 }
    613 
    614 static void
    615 add_rss_entry(char **rss_listing, struct article *a, char *url)
    616 {
    617 	if (!rss_listing || !a)
    618 		return;
    619 
    620 	struct tm    timeinfo;
    621 	char	    *content = NULL, *html_file = NULL,
    622 		    *link = NULL, pub_date[LONG_DATE_Z], *tmp = NULL;
    623 	const char  *entry_format =
    624 	    "%s<item>\n"
    625 	    "<guid>%s</guid>\n"
    626 	    "<link>%s</link>\n"
    627 	    "<author>%s</author>\n"
    628 	    "<title>%s</title>\n"
    629 	    "<pubDate>%s</pubDate>\n"
    630 	    "<description><![CDATA[\n%s\n]]></description>\n"
    631 	    "</item>\n";
    632 
    633 	if (strptime(a->date, "%Y-%m-%d", &timeinfo) == NULL)
    634 		goto out;
    635 
    636 	/*
    637 	 * we don't keep the hour and tz values on the markdown metadata, so set
    638 	 * it to 00h GMT.  Which will never be too off from reality.
    639 	 */
    640 	if (strftime(pub_date, LONG_DATE_Z, "%a, %d %b %Y 00:00:00 GMT", &timeinfo) == 0)
    641 		goto out;
    642 
    643 	content = strndup(a->html_content, a->htmlz);
    644 	html_file = strrchr(a->dst_path, '/') + 1;
    645 	if (html_file)
    646 		link = build_full_path(url, html_file);
    647 
    648 	if (link && content) {
    649 		/* keep the old listing address so we don't leak */
    650 		tmp = *rss_listing;
    651 		asprintf(rss_listing, entry_format, *rss_listing,
    652 		    link, link, a->author, a->title, pub_date, content);
    653 		free(tmp);
    654 	}
    655 
    656 out:
    657 	free(link);
    658 	free(content);
    659 }
    660 
    661 static const struct ssnail_error *
    662 write_rss(char *dst_path, char *rss_listing, char *title, char *url)
    663 {
    664 	const struct ssnail_error	*error = NULL;
    665 	struct tm			*timeinfo;
    666 	time_t				 now;
    667 	FILE				*fout;
    668 	char				*rss_path = NULL, *pub_date = NULL,
    669 					*rss_url = NULL;
    670 	char				*rss_format =
    671 	    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    672 	    "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n"
    673 	    "<channel>\n"
    674 	    "<atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />\n"
    675 	    "<title>%s</title>\n"
    676 	    "<link>%s</link>\n"
    677 	    "<lastBuildDate>%s</lastBuildDate>\n"
    678 	    "<description>%s</description>\n"
    679 	    "%s\n"
    680 	    "</channel>\n"
    681 	    "</rss>\n";
    682 
    683 	time(&now);
    684 	timeinfo = localtime(&now);
    685 	pub_date = malloc(LONG_DATE_Z);
    686 	if (strftime(pub_date, LONG_DATE_Z, "%a, %d %b %Y %H:%M:%S %z", timeinfo) == 0)
    687 		goto out;
    688 
    689 	if ((rss_path = build_full_path(dst_path, "rss.xml")) == NULL)
    690 		goto out;
    691 
    692 	if ((rss_url = build_full_path(url, "rss.xml")) == NULL)
    693 		goto out;
    694 
    695 	if ((fout = fopen(rss_path, "w")) == NULL) {
    696 		error = ssnail_error_from_errno("openw rss");
    697 		goto out;
    698 	}
    699 
    700 	int ret = fprintf(fout, rss_format, rss_url, title, url, pub_date,
    701 	    title, rss_listing);
    702 	if (ret < 0) {
    703 		error = ssnail_error_from_errno("write rss");
    704 		goto out;
    705 	}
    706 
    707 	if (fclose(fout) != 0)
    708 		error = ssnail_error_from_errno("close fout");
    709 
    710 out:
    711 	free(pub_date);
    712 	free(rss_path);
    713 	free(rss_url);
    714 	return error;
    715 }