Mercurial > noffle
diff database.c @ 0:04124a4423d4 noffle
[svn] Initial revision
author | enz |
---|---|
date | Tue, 04 Jan 2000 11:35:42 +0000 |
parents | |
children | 526a4c34ee2e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/database.c Tue Jan 04 11:35:42 2000 +0000 @@ -0,0 +1,609 @@ +/* + database.c + + $Id: database.c 3 2000-01-04 11:35:42Z enz $ + + Uses GNU gdbm library. Using Berkeley db (included in libc6) was + cumbersome. It is based on Berkeley db 1.85, which has severe bugs + (e.g. it is not recommended to delete or overwrite entries with + overflow pages). +*/ + +#include "database.h" +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <gdbm.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include "config.h" +#include "log.h" +#include "protocol.h" +#include "util.h" + +static struct Db +{ + GDBM_FILE dbf; + + /* Start string for Xref header line: "Xref: <host>" */ + Str xrefHost; + + /* Msg Id of presently loaded article, empty if none loaded */ + Str msgId; + + /* Status of loaded article */ + int stat; /* Flags */ + time_t lastAccess; + + /* Overview of loaded article */ + Str subj; + Str from; + Str date; + Str ref; + Str xref; + size_t bytes; + size_t lines; + + /* Article text (except for overview header lines) */ + DynStr *txt; + +} db = { NULL, "(unknown)", "", 0, 0, "", "", "", "", "", 0, 0, NULL }; + +static const char * +errMsg( void ) +{ + if ( errno != 0 ) + return strerror( errno ); + return gdbm_strerror( gdbm_errno ); +} + +Bool +Db_open( void ) +{ + Str name, host; + int flags; + + ASSERT( db.dbf == NULL ); + snprintf( name, MAXCHAR, "%s/data/articles.gdbm", Cfg_spoolDir() ); + flags = GDBM_WRCREAT | GDBM_FAST; + + if ( ! ( db.dbf = gdbm_open( name, 512, flags, 0644, NULL ) ) ) + { + Log_err( "Error opening %s for r/w (%s)", name, errMsg() ); + return FALSE; + } + Log_dbg( "%s opened for r/w", name ); + + if ( db.txt == NULL ) + db.txt = new_DynStr( 5000 ); + + gethostname( host, MAXCHAR ); + snprintf( db.xrefHost, MAXCHAR, "Xref: %s", host ); + + return TRUE; +} + +void +Db_close( void ) +{ + ASSERT( db.dbf ); + Log_dbg( "Closing database" ); + gdbm_close( db.dbf ); + db.dbf = NULL; + del_DynStr( db.txt ); + db.txt = NULL; + Utl_cpyStr( db.msgId, "" ); +} + +static Bool +loadArt( const char *msgId ) +{ + static void *dptr = NULL; + + datum key, val; + Str t = ""; + const char *p; + + ASSERT( db.dbf ); + + if ( strcmp( msgId, db.msgId ) == 0 ) + return TRUE; + + key.dptr = (void *)msgId; + key.dsize = strlen( msgId ) + 1; + if ( dptr != NULL ) + { + free( dptr ); + dptr = NULL; + } + val = gdbm_fetch( db.dbf, key ); + dptr = val.dptr; + if ( dptr == NULL ) + { + Log_dbg( "database.c loadArt: gdbm_fetch found no entry" ); + return FALSE; + } + + Utl_cpyStr( db.msgId, msgId ); + p = Utl_getLn( t, (char *)dptr ); + if ( ! p || sscanf( t, "%x", &db.stat ) != 1 ) + { + Log_err( "Entry in database '%s' is corrupt (status)", msgId ); + return FALSE; + } + p = Utl_getLn( t, p ); + if ( ! p || sscanf( t, "%lu", &db.lastAccess ) != 1 ) + { + Log_err( "Entry in database '%s' is corrupt (lastAccess)", msgId ); + return FALSE; + } + p = Utl_getLn( db.subj, p ); + p = Utl_getLn( db.from, p ); + p = Utl_getLn( db.date, p ); + p = Utl_getLn( db.ref, p ); + p = Utl_getLn( db.xref, p ); + if ( ! p ) + { + Log_err( "Entry in database '%s' is corrupt (overview)", msgId ); + return FALSE; + } + p = Utl_getLn( t, p ); + if ( ! p || sscanf( t, "%u", &db.bytes ) != 1 ) + { + Log_err( "Entry in database '%s' is corrupt (bytes)", msgId ); + return FALSE; + } + p = Utl_getLn( t, p ); + if ( ! p || sscanf( t, "%u", &db.lines ) != 1 ) + { + Log_err( "Entry in database '%s' is corrupt (lines)", msgId ); + return FALSE; + } + DynStr_clear( db.txt ); + DynStr_app( db.txt, p ); + return TRUE; +} + +static Bool +saveArt( void ) +{ + DynStr *s; + Str t = ""; + datum key, val; + + if ( strcmp( db.msgId, "" ) == 0 ) + return FALSE; + s = new_DynStr( 5000 ); + snprintf( t, MAXCHAR, "%x", db.stat ); + DynStr_appLn( s, t ); + snprintf( t, MAXCHAR, "%lu", db.lastAccess ); + DynStr_appLn( s, t ); + DynStr_appLn( s, db.subj ); + DynStr_appLn( s, db.from ); + DynStr_appLn( s, db.date ); + DynStr_appLn( s, db.ref ); + DynStr_appLn( s, db.xref ); + snprintf( t, MAXCHAR, "%u", db.bytes ); + DynStr_appLn( s, t ); + snprintf( t, MAXCHAR, "%u", db.lines ); + DynStr_appLn( s, t ); + DynStr_appDynStr( s, db.txt ); + + key.dptr = (void *)db.msgId; + key.dsize = strlen( db.msgId ) + 1; + val.dptr = (void *)DynStr_str( s ); + val.dsize = DynStr_len( s ) + 1; + if ( gdbm_store( db.dbf, key, val, GDBM_REPLACE ) != 0 ) + { + Log_err( "Could not store %s in database (%s)", errMsg() ); + return FALSE; + } + + del_DynStr( s ); + return TRUE; +} + +Bool +Db_prepareEntry( const Over *ov, const char *grp, int numb ) +{ + const char *msgId; + + ASSERT( db.dbf ); + ASSERT( ov ); + ASSERT( grp ); + + msgId = Ov_msgId( ov ); + Log_dbg( "Preparing entry %s", msgId ); + if ( Db_contains( msgId ) ) + Log_err( "Preparing article twice: %s", msgId ); + + db.stat = DB_NOT_DOWNLOADED; + db.lastAccess = time( NULL ); + + Utl_cpyStr( db.msgId, msgId ); + Utl_cpyStr( db.subj, Ov_subj( ov ) ); + Utl_cpyStr( db.from, Ov_from( ov ) ); + Utl_cpyStr( db.date, Ov_date( ov ) ); + Utl_cpyStr( db.ref, Ov_ref( ov ) ); + snprintf( db.xref, MAXCHAR, "%s:%i", grp, numb ); + db.bytes = Ov_bytes( ov ); + db.lines = Ov_lines( ov ); + + DynStr_clear( db.txt ); + + return saveArt(); +} + +Bool +Db_storeArt( const char *msgId, const char *artTxt ) +{ + Str line, lineEx, field, value; + const char *startPos; + + ASSERT( db.dbf ); + + Log_dbg( "Store article %s", msgId ); + if ( ! loadArt( msgId ) ) + { + Log_err( "Cannot find info about '%s' in database", msgId ); + return FALSE; + } + if ( ! ( db.stat & DB_NOT_DOWNLOADED ) ) + { + Log_err( "Trying to store alrady retrieved article '%s'", msgId ); + return FALSE; + } + db.stat &= ~DB_NOT_DOWNLOADED; + db.stat &= ~DB_RETRIEVING_FAILED; + db.lastAccess = time( NULL ); + + DynStr_clear( db.txt ); + + /* Read header */ + startPos = artTxt; + while ( TRUE ) + { + artTxt = Utl_getLn( lineEx, artTxt ); + if ( lineEx[ 0 ] == '\0' ) + { + DynStr_appLn( db.txt, lineEx ); + break; + } + /* Get other lines if field is split over multiple lines */ + while ( ( artTxt = Utl_getLn( line, artTxt ) ) ) + if ( isspace( line[ 0 ] ) ) + { + strncat( lineEx, "\n", MAXCHAR ); + strncat( lineEx, line, MAXCHAR ); + } + else + { + artTxt = Utl_ungetLn( startPos, artTxt ); + break; + } + /* Remove fields already in overview and handle x-noffle + headers correctly in case of cascading NOFFLEs */ + if ( Prt_getField( field, value, lineEx ) ) + { + if ( strcmp( field, "x-noffle-status" ) == 0 ) + { + if ( strstr( value, "NOT_DOWNLOADED" ) != 0 ) + db.stat |= DB_NOT_DOWNLOADED; + } + else if ( strcmp( field, "message-id" ) != 0 + && strcmp( field, "xref" ) != 0 + && strcmp( field, "references" ) != 0 + && strcmp( field, "subject" ) != 0 + && strcmp( field, "from" ) != 0 + && strcmp( field, "date" ) != 0 + && strcmp( field, "bytes" ) != 0 + && strcmp( field, "lines" ) != 0 + && strcmp( field, "x-noffle-lastaccess" ) != 0 ) + DynStr_appLn( db.txt, lineEx ); + } + } + + /* Read body */ + while ( ( artTxt = Utl_getLn( line, artTxt ) ) ) + if ( ! ( db.stat & DB_NOT_DOWNLOADED ) ) + DynStr_appLn( db.txt, line ); + + return saveArt(); +} + +void +Db_setStat( const char *msgId, int stat ) +{ + if ( loadArt( msgId ) ) + { + db.stat = stat; + saveArt(); + } +} + +void +Db_updateLastAccess( const char *msgId ) +{ + if ( loadArt( msgId ) ) + { + db.lastAccess = time( NULL ); + saveArt(); + } +} + +void +Db_setXref( const char *msgId, const char *xref ) +{ + if ( loadArt( msgId ) ) + { + Utl_cpyStr( db.xref, xref ); + saveArt(); + } +} + +/* Search best position for breaking a line */ +static const char * +searchBreakPos( const char *line, int wantedLength ) +{ + const char *lastSpace = NULL; + Bool lastWasSpace = FALSE; + int len = 0; + + while ( *line != '\0' ) + { + if ( isspace( *line ) ) + { + if ( len > wantedLength && lastSpace != NULL ) + return lastSpace; + if ( ! lastWasSpace ) + lastSpace = line; + lastWasSpace = TRUE; + } + else + lastWasSpace = FALSE; + ++len; + ++line; + } + if ( len > wantedLength && lastSpace != NULL ) + return lastSpace; + return line; +} + +/* Append header line by breaking long line into multiple lines */ +static void +appendLongHeader( DynStr *target, const char *field, const char *value ) +{ + const int wantedLength = 78; + const char *breakPos, *old; + int len; + + len = strlen( field ); + DynStr_appN( target, field, len ); + DynStr_appN( target, " ", 1 ); + old = value; + while ( isspace( *old ) ) + ++old; + breakPos = searchBreakPos( old, wantedLength - len - 1 ); + DynStr_appN( target, old, breakPos - old ); + if ( *breakPos == '\0' ) + { + DynStr_appN( target, "\n", 1 ); + return; + } + DynStr_appN( target, "\n ", 2 ); + while ( TRUE ) + { + old = breakPos; + while ( isspace( *old ) ) + ++old; + breakPos = searchBreakPos( old, wantedLength - 1 ); + DynStr_appN( target, old, breakPos - old ); + if ( *breakPos == '\0' ) + { + DynStr_appN( target, "\n", 1 ); + return; + } + DynStr_appN( target, "\n ", 2 ); + } +} + +const char * +Db_header( const char *msgId ) +{ + static DynStr *s = NULL; + + Str date, t; + int stat; + const char *p; + + if ( s == NULL ) + s = new_DynStr( 5000 ); + else + DynStr_clear( s ); + ASSERT( db.dbf ); + if ( ! loadArt( msgId ) ) + return NULL; + strftime( date, MAXCHAR, "%Y-%m-%d %H:%M:%S", + localtime( &db.lastAccess ) ); + stat = db.stat; + snprintf( t, MAXCHAR, + "Message-ID: %s\n" + "X-NOFFLE-Status:%s%s%s\n" + "X-NOFFLE-LastAccess: %s\n", + msgId, + stat & DB_INTERESTING ? " INTERESTING" : "", + stat & DB_NOT_DOWNLOADED ? " NOT_DOWNLOADED" : "", + stat & DB_RETRIEVING_FAILED ? " RETRIEVING_FAILED" : "", + date ); + DynStr_app( s, t ); + appendLongHeader( s, "Subject:", db.subj ); + appendLongHeader( s, "From:", db.from ); + appendLongHeader( s, "Date:", db.date ); + appendLongHeader( s, "References:", db.ref ); + DynStr_app( s, "Bytes: " ); + snprintf( t, MAXCHAR, "%u", db.bytes ); + DynStr_appLn( s, t ); + DynStr_app( s, "Lines: " ); + snprintf( t, MAXCHAR, "%u", db.lines ); + DynStr_appLn( s, t ); + appendLongHeader( s, db.xrefHost, db.xref ); + p = strstr( DynStr_str( db.txt ), "\n\n" ); + if ( ! p ) + DynStr_appDynStr( s, db.txt ); + else + DynStr_appN( s, DynStr_str( db.txt ), p - DynStr_str( db.txt ) + 1 ); + return DynStr_str( s ); +} + +const char * +Db_body( const char *msgId ) +{ + const char *p; + + if ( ! loadArt( msgId ) ) + return ""; + p = strstr( DynStr_str( db.txt ), "\n\n" ); + if ( ! p ) + return ""; + return ( p + 2 ); +} + +int +Db_stat( const char *msgId ) +{ + if ( ! loadArt( msgId ) ) + return 0; + return db.stat; +} + +time_t +Db_lastAccess( const char *msgId ) +{ + if ( ! loadArt( msgId ) ) + return -1; + return db.lastAccess; +} + +const char * +Db_ref( const char *msgId ) +{ + if ( ! loadArt( msgId ) ) + return ""; + return db.ref; +} + +const char * +Db_xref( const char *msgId ) +{ + if ( ! loadArt( msgId ) ) + return ""; + return db.xref; +} + +Bool +Db_contains( const char *msgId ) +{ + datum key; + + ASSERT( db.dbf ); + if ( strcmp( msgId, db.msgId ) == 0 ) + return TRUE; + key.dptr = (void*)msgId; + key.dsize = strlen( msgId ) + 1; + return gdbm_exists( db.dbf, key ); +} + +static datum cursor = { NULL, 0 }; + +Bool +Db_first( const char** msgId ) +{ + ASSERT( db.dbf ); + if ( cursor.dptr != NULL ) + { + free( cursor.dptr ); + cursor.dptr = NULL; + } + cursor = gdbm_firstkey( db.dbf ); + *msgId = cursor.dptr; + return ( cursor.dptr != NULL ); +} + +Bool +Db_next( const char** msgId ) +{ + void *oldDptr = cursor.dptr; + + ASSERT( db.dbf ); + if ( cursor.dptr == NULL ) + return FALSE; + cursor = gdbm_nextkey( db.dbf, cursor ); + free( oldDptr ); + *msgId = cursor.dptr; + return ( cursor.dptr != NULL ); +} + +Bool +Db_expire( unsigned int days ) +{ + double limit; + int cntDel, cntLeft, flags; + time_t nowTime, lastAccess; + const char *msgId; + Str name, tmpName; + GDBM_FILE tmpDbf; + datum key, val; + + if ( ! Db_open() ) + return FALSE; + snprintf( name, MAXCHAR, "%s/data/articles.gdbm", Cfg_spoolDir() ); + snprintf( tmpName, MAXCHAR, "%s/data/articles.gdbm.new", Cfg_spoolDir() ); + flags = GDBM_NEWDB | GDBM_FAST; + if ( ! ( tmpDbf = gdbm_open( tmpName, 512, flags, 0644, NULL ) ) ) + { + Log_err( "Error opening %s for read/write (%s)", errMsg() ); + Db_close(); + return FALSE; + } + Log_inf( "Expiring articles that have not been accessed for %u days", + days ); + limit = days * 24. * 3600.; + cntDel = 0; + cntLeft = 0; + nowTime = time( NULL ); + if ( Db_first( &msgId ) ) + do + { + lastAccess = Db_lastAccess( msgId ); + if ( lastAccess == -1 ) + Log_err( "Internal error: Getting lastAccess of %s failed", + msgId ); + else if ( difftime( nowTime, lastAccess ) > limit ) + { + Log_dbg( "Expiring %s", msgId ); + ++cntDel; + } + else + { + ++cntLeft; + key.dptr = (void *)msgId; + key.dsize = strlen( msgId ) + 1; + + val = gdbm_fetch( db.dbf, key ); + if ( val.dptr != NULL ) + { + if ( gdbm_store( tmpDbf, key, val, GDBM_INSERT ) != 0 ) + Log_err( "Could not store %s in new database (%s)", + errMsg() ); + free( val.dptr ); + } + } + } + while ( Db_next( &msgId ) ); + Log_inf( "%lu articles deleted, %lu left", cntDel, cntLeft ); + gdbm_close( tmpDbf ); + Db_close(); + rename( tmpName, name ); + return TRUE; +}