1 /* $Id: expire.c 6135 2003-01-19 01:15:40Z rra $
3 ** Expire news articles.
15 #include "inn/history.h"
16 #include "inn/innconf.h"
17 #include "inn/messages.h"
24 typedef struct _EXPIRECLASS {
33 ** Expire-specific stuff.
35 #define MAGIC_TIME 49710.
37 static bool EXPtracing;
38 static bool EXPusepost;
39 static bool Ignoreselfexpire = false;
40 static FILE *EXPunlinkfile;
41 static EXPIRECLASS EXPclasses[NUM_STORAGE_CLASSES+1];
42 static char *EXPreason;
43 static time_t EXPremember;
45 static time_t RealNow;
47 /* Statistics; for -v flag. */
48 static char *EXPgraph;
49 static int EXPverbose;
50 static long EXPprocessed;
51 static long EXPunlinked;
52 static long EXPallgone;
53 static long EXPstillhere;
54 static struct history *History;
55 static char *NHistory;
57 static void CleanupAndExit(bool Server, bool Paused, int x);
59 static int EXPsplit(char *p, char sep, char **argv, int count);
61 enum KR {Keep, Remove};
66 ** Open a file or give up.
69 EXPfopen(bool Unlink, const char *Name, const char *Mode, bool Needclean,
70 bool Server, bool Paused)
74 if (Unlink && unlink(Name) < 0 && errno != ENOENT)
75 syswarn("cannot remove %s", Name);
76 if ((F = fopen(Name, Mode)) == NULL) {
77 syswarn("cannot open %s in %s mode", Name, Mode);
79 CleanupAndExit(Server, Paused, 1);
88 ** Split a line at a specified field separator into a vector and return
89 ** the number of fields found, or -1 on error.
91 static int EXPsplit(char *p, char sep, char **argv, int count)
113 for (i = 1, *argv++ = p; *p; )
116 for (; *p == sep; p++)
130 ** Parse a number field converting it into a "when did this start?".
131 ** This makes the "keep it" tests fast, but inverts the logic of
132 ** just about everything you expect. Print a message and return false
135 static bool EXPgetnum(int line, char *word, time_t *v, const char *name)
141 if (strcasecmp(word, "never") == 0) {
146 /* Check the number. We don't have strtod yet. */
147 for (p = word; ISWHITE(*p); p++)
149 if (*p == '+' || *p == '-')
151 for (SawDot = false; *p; p++)
157 else if (!CTYPE(isdigit, (int)*p))
160 warn("bad '%c' character in %s field on line %d", *p, name, line);
167 *v = Now - (time_t)(d * 86400.);
173 ** Parse the expiration control file. Return true if okay.
175 static bool EXPreadfile(FILE *F)
184 /* Scan all lines. */
188 for (i = 0; i <= NUM_STORAGE_CLASSES; i++) {
189 EXPclasses[i].ReportedMissing = false;
190 EXPclasses[i].Missing = true;
193 for (i = 1; fgets(buff, sizeof buff, F) != NULL; i++) {
194 if ((p = strchr(buff, '\n')) == NULL) {
195 warn("line %d too long", i);
199 p = strchr(buff, '#');
203 p = buff + strlen(buff);
204 while (--p >= buff) {
205 if (isspace((int)*p))
212 if ((j = EXPsplit(buff, ':', fields, ARRAY_SIZE(fields))) == -1) {
213 warn("too many fields on line %d", i);
217 /* Expired-article remember line? */
218 if (strcmp(fields[0], "/remember/") == 0) {
220 warn("invalid format on line %d", i);
223 if (EXPremember != -1) {
224 warn("duplicate /remember/ on line %d", i);
227 if (!EXPgetnum(i, fields[1], &EXPremember, "remember"))
232 /* Storage class line? */
234 /* Is this the default line? */
235 if (fields[0][0] == '*' && fields[0][1] == '\0') {
237 warn("duplicate default on line %d", i);
240 j = NUM_STORAGE_CLASSES;
244 if ((j < 0) || (j >= NUM_STORAGE_CLASSES))
245 warn("bad storage class %d on line %d", j, i);
248 if (!EXPgetnum(i, fields[1], &EXPclasses[j].Keep, "keep")
249 || !EXPgetnum(i, fields[2], &EXPclasses[j].Default, "default")
250 || !EXPgetnum(i, fields[3], &EXPclasses[j].Purge, "purge"))
252 /* These were turned into offsets, so the test is the opposite
253 * of what you think it should be. If Purge isn't forever,
254 * make sure it's greater then the other two fields. */
255 if (EXPclasses[j].Purge) {
256 /* Some value not forever; make sure other values are in range. */
257 if (EXPclasses[j].Keep && EXPclasses[j].Keep < EXPclasses[j].Purge) {
258 warn("keep time longer than purge time on line %d", i);
261 if (EXPclasses[j].Default && EXPclasses[j].Default < EXPclasses[j].Purge) {
262 warn("default time longer than purge time on line %d", i);
266 EXPclasses[j].Missing = false;
270 /* Regular expiration line -- right number of fields? */
272 warn("bad format on line %d", i);
275 continue; /* don't process this line--per-group expiry is done by expireover */
282 ** Should we keep the specified article?
284 static enum KR EXPkeepit(const TOKEN *token, time_t when, time_t Expires)
288 class = EXPclasses[token->class];
290 if (EXPclasses[NUM_STORAGE_CLASSES].Missing) {
292 if (!class.ReportedMissing) {
293 warn("class definition for %d missing from control file,"
294 " assuming it should never expire", token->class);
295 EXPclasses[token->class].ReportedMissing = true;
299 /* use the default */
300 class = EXPclasses[NUM_STORAGE_CLASSES];
301 EXPclasses[token->class] = class;
304 /* Bad posting date? */
305 if (when > (RealNow + 86400)) {
306 /* Yes -- force the article to go to right now */
307 when = Expires ? class.Purge : class.Default;
309 if (EXPverbose > 2) {
311 printf("%s age = %0.2f\n", TokenToText(*token), (Now - when) / 86400.);
313 if (when <= class.Default)
314 printf("%s too old (no exp)\n", TokenToText(*token));
316 if (when <= class.Purge)
317 printf("%s later than purge\n", TokenToText(*token));
318 if (when >= class.Keep)
319 printf("%s earlier than min\n", TokenToText(*token));
321 printf("%s later than header\n", TokenToText(*token));
325 /* If no expiration, make sure it wasn't posted before the default. */
327 if (when >= class.Default)
330 /* Make sure it's not posted before the purge cut-off and
331 * that it's not due to expire. */
333 if (when >= class.Purge && (Expires >= Now || when >= class.Keep))
342 ** An article can be removed. Either print a note, or actually remove it.
343 ** Also fill in the article size.
346 EXPremove(const TOKEN *token)
348 /* Turn into a filename and get the size if we need it. */
350 printf("\tunlink %s\n", TokenToText(*token));
354 printf("%s\n", TokenToText(*token));
360 fprintf(EXPunlinkfile, "%s\n", TokenToText(*token));
361 if (!ferror(EXPunlinkfile))
363 syswarn("cannot write to -z file (will ignore it for rest of run)");
364 fclose(EXPunlinkfile);
365 EXPunlinkfile = NULL;
367 if (!SMcancel(*token) && SMerrno != SMERR_NOENT && SMerrno != SMERR_UNINIT)
368 warn("cannot unlink %s", TokenToText(*token));
372 ** Do the work of expiring one line.
375 EXPdoline(void *cookie UNUSED, time_t arrived, time_t posted, time_t expires,
379 bool HasSelfexpire = false;
380 bool Selfexpired = false;
385 if (innconf->groupbaseexpiry || SMprobe(SELFEXPIRE, token, NULL)) {
386 if ((article = SMretrieve(*token, RETR_STAT)) == (ARTHANDLE *)NULL) {
387 HasSelfexpire = true;
390 /* the article is still alive */
391 SMfreearticle(article);
392 if (innconf->groupbaseexpiry || !Ignoreselfexpire)
393 HasSelfexpire = true;
396 if (EXPusepost && posted != 0)
403 if (Selfexpired || token->type == TOKEN_EMPTY) {
411 kr = EXPkeepit(token, when, expires);
428 ** Clean up link with the server and exit.
431 CleanupAndExit(bool Server, bool Paused, int x)
437 if (Paused && ICCgo(EXPreason) != 0) {
438 syswarn("cannot unpause server");
442 if (Server && ICCclose() < 0) {
443 syswarn("cannot close communication link to server");
446 if (EXPunlinkfile && fclose(EXPunlinkfile) == EOF) {
447 syswarn("cannot close -z file");
453 printf("Article lines processed %8ld\n", EXPprocessed);
454 printf("Articles retained %8ld\n", EXPstillhere);
455 printf("Entries expired %8ld\n", EXPallgone);
456 if (!innconf->groupbaseexpiry)
457 printf("Articles dropped %8ld\n", EXPunlinked);
460 /* Append statistics to a summary file */
462 F = EXPfopen(false, EXPgraph, "a", false, false, false);
463 fprintf(F, "%ld %ld %ld %ld %ld\n",
464 (long)Now, EXPprocessed, EXPstillhere, EXPallgone,
471 if (EXPreason != NULL)
474 if (NHistory != NULL)
481 ** Print a usage message and exit.
486 fprintf(stderr, "Usage: expire [flags] [expire.ctl]\n");
492 ** Change to the news user if possible, and if not, die. Used for operations
493 ** that may create new database files so as not to mess up the ownership.
500 pwd = getpwnam(NEWSUSER);
502 die("can't resolve %s to a UID (account doesn't exist?)", NEWSUSER);
505 if (getuid() != pwd->pw_uid)
506 die("must be run as %s", NEWSUSER);
511 main(int ac, char *av[])
517 const char *NHistoryPath = NULL;
518 const char *NHistoryText = NULL;
530 /* First thing, set up logging and our identity. */
531 openlog("expire", L_OPENLOG_FLAGS | LOG_PID, LOG_INN_PROG);
532 message_program_name = "expire";
541 if (!innconf_read(NULL))
544 HistoryText = concatpath(innconf->pathdb, _PATH_HISTORY);
548 /* find the default history file directory */
549 EXPhistdir = xstrdup(HistoryText);
550 p = strrchr(EXPhistdir, '/');
556 while ((i = getopt(ac, av, "f:h:d:g:iNnpr:s:tv:w:xz:")) != EOF)
562 NHistoryPath = optarg;
565 NHistoryText = optarg;
571 HistoryText = optarg;
577 Ignoreselfexpire = true;
586 EXPreason = xstrdup(optarg);
595 EXPverbose = atoi(optarg);
598 TimeWarp = (time_t)(atof(optarg) * 86400.);
604 EXPunlinkfile = EXPfopen(true, optarg, "a", false, false, false);
610 if ((ac != 0 && ac != 1))
613 /* if EXPtracing is set, then pass in a path, this ensures we
614 * don't replace the existing history files */
615 if (EXPtracing || NHistoryText || NHistoryPath) {
616 if (NHistoryPath == NULL)
617 NHistoryPath = innconf->pathdb;
618 if (NHistoryText == NULL)
619 NHistoryText = _PATH_HISTORY;
620 NHistory = concatpath(NHistoryPath, NHistoryText);
630 /* Change users if necessary. */
633 /* Parse the control file. */
635 if (strcmp(av[0], "-") == 0)
638 F = EXPfopen(false, av[0], "r", false, false, false);
642 path = concatpath(innconf->pathetc, _PATH_EXPIRECTL);
643 F = EXPfopen(false, path, "r", false, false, false);
646 if (!EXPreadfile(F)) {
648 die("format error in expire.ctl");
652 /* Set up the link, reserve the lock. */
654 if (EXPreason == NULL) {
655 snprintf(buff, sizeof(buff), "Expiring process %ld",
657 EXPreason = xstrdup(buff);
665 /* If we fail, leave evidence behind. */
667 syswarn("cannot open channel to server");
668 CleanupAndExit(false, false, 1);
670 if (ICCreserve((char *)EXPreason) != 0) {
671 warn("cannot reserve server");
672 CleanupAndExit(false, false, 1);
676 History = HISopen(HistoryText, innconf->hismethod, HIS_RDONLY);
678 warn("cannot open history");
679 CleanupAndExit(Server, false, 1);
682 /* Ignore failure on the HISctl()s, if the underlying history
683 * manager doesn't implement them its not a disaster */
684 HISctl(History, HISCTLS_IGNOREOLD, &IgnoreOld);
686 HISctl(History, HISCTLS_NPAIRS, &Size);
690 if (!SMsetup(SM_RDWR, (void *)&val) || !SMsetup(SM_PREOPEN, (void *)&val)) {
691 warn("cannot set up storage manager");
692 CleanupAndExit(Server, false, 1);
695 warn("cannot initialize storage manager: %s", SMerrorstr);
696 CleanupAndExit(Server, false, 1);
698 if (chdir(EXPhistdir) < 0) {
699 syswarn("cannot chdir to %s", EXPhistdir);
700 CleanupAndExit(Server, false, 1);
703 Bad = HISexpire(History, NHistory, EXPreason, Writing, NULL,
704 EXPremember, EXPdoline) == false;
706 if (UnlinkFile && EXPunlinkfile == NULL)
707 /* Got -z but file was closed; oops. */
710 /* If we're done okay, and we're not tracing, slip in the new files. */
713 printf("Expire errors: history files not updated.\n");
715 printf("Expire tracing: history files not updated.\n");
718 if (!Bad && NHistory != NULL) {
719 snprintf(buff, sizeof(buff), "%s.n.done", NHistory);
720 fclose(EXPfopen(false, buff, "w", true, Server, false));
721 CleanupAndExit(Server, false, Bad ? 1 : 0);
724 CleanupAndExit(Server, !Bad, Bad ? 1 : 0);