/*  $Revision: 1.23 $
**
**  History file routines.
*/
#include <stdio.h>
#include <sys/types.h>
#include "configdata.h"
#include "clibrary.h"
#include "innd.h"
#include "dbz.h"


STATIC char	HIShistpath[] = _PATH_HISTORY;
STATIC FILE	*HISwritefp;
STATIC int	HISreadfd;
STATIC int	HISdirty;
STATIC int	HISincore = INND_DBZINCORE;


/*
**  Set up the history files.
*/
void
HISsetup()
{
    if (HISwritefp == NULL) {
	/* Open the history file for appending formatted I/O. */
	if ((HISwritefp = fopen(HIShistpath, "a")) == NULL) {
	    syslog(L_FATAL, "%s cant fopen %s %m", LogName, HIShistpath);
	    exit(1);
	}
	CloseOnExec((int)fileno(HISwritefp), TRUE);

	/* Open the history file for reading. */
	if ((HISreadfd = open(HIShistpath, O_RDONLY)) < 0) {
	    syslog(L_FATAL, "%s cant open %s %m", LogName, HIShistpath);
	    exit(1);
	}
	CloseOnExec(HISreadfd, TRUE);

	/* Open the DBZ file. */
	(void)dbzincore(HISincore);
	(void)dbzwritethrough(1);
	if (dbminit(HIShistpath) < 0) {
	    syslog(L_FATAL, "%s cant dbminit %s %m", HIShistpath, LogName);
	    exit(1);
	}
    }
}


/*
**  Synchronize the in-core history file (flush it).
*/
void
HISsync()
{
    if (HISdirty) {
	if (dbzsync()) {
	    syslog(L_FATAL, "%s cant dbzsync %m", LogName);
	    exit(1);
	}
	HISdirty = 0;
    }
}


/*
**  Close the history files.
*/
void
HISclose()
{
    if (HISwritefp != NULL) {
	/* Since dbmclose calls dbzsync we could replace this line with
	 * "HISdirty = 0;".  Oh well, it keeps the abstraction clean. */
	HISsync();
	if (dbmclose() < 0)
	    syslog(L_ERROR, "%s cant dbmclose %m", LogName);
	if (fclose(HISwritefp) == EOF)
	    syslog(L_ERROR, "%s cant fclose history %m", LogName);
	HISwritefp = NULL;
	if (close(HISreadfd) < 0)
	    syslog(L_ERROR, "%s cant close history %m", LogName);
	HISreadfd = -1;
    }
}


/*
**  File in the DBZ datum for a Message-ID, making sure not to copy any
**  illegal characters.
*/
STATIC void
HISsetkey(p, keyp)
    register char	*p;
    datum		*keyp;
{
    static BUFFER	MessageID;
    register char	*dest;
    register int	i;

    /* Get space to hold the ID. */
    i = strlen(p);
    if (MessageID.Data == NULL) {
	MessageID.Data = NEW(char, i + 1);
	MessageID.Size = i;
    }
    else if (MessageID.Size < i) {
	RENEW(MessageID.Data, char, i + 1);
	MessageID.Size = i;
    }

    for (keyp->dptr = dest = MessageID.Data; *p; p++)
	if (*p == HIS_FIELDSEP || *p == '\n')
	    *dest++ = HIS_BADCHAR;
	else
	    *dest++ = *p;
    *dest = '\0';

    keyp->dsize = dest - MessageID.Data + 1;
}


/*
**  Get the list of files under which a Message-ID is stored.
*/
char *
HISfilesfor(MessageID)
    char		*MessageID;
{
    static BUFFER	Files;
    char		*dest;
    datum		key;
    datum		val;
    long		offset;
    register char	*p;
    register int	i;

    /* Get the seek value into the history file. */
    HISsetkey(MessageID, &key);
    val = dbzfetch(key);
    if (val.dptr == NULL || val.dsize != sizeof offset)
	return NULL;

    /* Get space. */
    if (Files.Data == NULL) {
	Files.Size = BUFSIZ;
	Files.Data = NEW(char, Files.Size);
    }

    /* Copy the value to an aligned spot. */
    for (p = val.dptr, dest = (char *)&offset, i = sizeof offset; --i >= 0; )
	*dest++ = *p++;
    if (lseek(HISreadfd, offset, SEEK_SET) == -1)
	return NULL;

    /* Read the text until \n or EOF. */
    for (Files.Used = 0; ; ) {
	i = read(HISreadfd,
		&Files.Data[Files.Used], Files.Size - Files.Used - 1);
	if (i <= 0)
	    return NULL;
	Files.Used += i;
	Files.Data[Files.Used] = '\0';
	if ((p = strchr(Files.Data, '\n')) != NULL) {
	    *p = '\0';
	    break;
	}

	/* If we have half our buffer left, get more space. */
	if (Files.Size - Files.Used < Files.Size / 2) {
	    Files.Size += BUFSIZ;
	    RENEW(Files.Data, char, Files.Size);
	}
    }

    /* Move past the first two fields -- Message-ID and date info. */
    if ((p = strchr(Files.Data, HIS_FIELDSEP)) == NULL)
	return NULL;
    if ((p = strchr(p + 1, HIS_FIELDSEP)) == NULL)
	return NULL;

    /* Translate newsgroup separators to slashes, return the fieldstart. */
    for (dest = ++p; *p; p++)
	if (*p == '.')
	    *p = '/';
    return dest;
}


/*
**  Have we already seen an article?
*/
BOOL
HIShavearticle(MessageID)
    char	*MessageID;
{
    datum	key;
    datum	val;

    HISsetkey(MessageID, &key);
    val = dbzfetch(key);
    return val.dptr != NULL;
}


/*
**  Turn a history filename entry from slashes to dots.  It's a pity
**  we have to do this.
*/
STATIC void
HISslashify(p)
    register char	*p;
{
    register char	*last;

    for (last = NULL; *p; p++) {
	if (*p == '/') {
	    *p = '.';
	    last = p;
	}
	else if (*p == ' ' && last != NULL)
	    *last = '/';
    }
    if (last)
	*last = '/';
}


/*
**  Write a history entry.
*/
BOOL
HISwrite(Data, paths)
    ARTDATA		*Data;
    char		*paths;
{
    static char		NOPATHS[] = "";
    long		offset;
    datum		key;
    datum		val;
    int			i;

    HISsetkey(Data->MessageID, &key);
    if (paths != NULL && paths[0] != '\0')
	HISslashify(paths);
    else
	paths = NOPATHS;

    offset = ftell(HISwritefp);
    if (Data->Expires > 0)
	i = fprintf(HISwritefp, "%s%c%ld%c%ld%c%ld%c%s\n",
		key.dptr, HIS_FIELDSEP,
		(long)Data->Arrived, HIS_SUBFIELDSEP, (long)Data->Expires,
		    HIS_SUBFIELDSEP, (long)Data->Posted, HIS_FIELDSEP,
		paths);
    else
	i = fprintf(HISwritefp, "%s%c%ld%c%s%c%ld%c%s\n",
		key.dptr, HIS_FIELDSEP,
		(long)Data->Arrived, HIS_SUBFIELDSEP, HIS_NOEXP,
		    HIS_SUBFIELDSEP, (long)Data->Posted, HIS_FIELDSEP,
		paths);
    if (i == EOF || fflush(HISwritefp) == EOF) {
	/* The history line is now an orphan... */
	i = errno;
	syslog(L_ERROR, "%s cant write history %m", LogName);
	IOError("history", i);
	return FALSE;
    }

    /* Set up the database values and write them. */
    val.dptr = (char *)&offset;
    val.dsize = sizeof offset;
    if (dbzstore(key, val) < 0) {
	i = errno;
	syslog(L_ERROR, "%s cant dbzstore %m", LogName);
	IOError("history database", i);
	return FALSE;
    }

    if (++HISdirty >= ICD_SYNC_COUNT)
	HISsync();
    return TRUE;
}
