2 * FILE: $Header: /home/egg/src/RCS/storage.c,v 1.4 1998/12/31 22:07:56 ghn Exp $
3 * PURPOSE: Data storage functions
9 * Revision 1.4 1998/12/31 22:07:56 ghn
10 * Rev 5 code: includes multi-reg support, HTML, etc.
12 * Revision 1.3 1998/08/03 20:32:46 kelvin
13 * File byte-order independence, STORAGE_DEBUG
15 * Revision 1.2 1998/08/01 17:18:45 ghn
16 * Fixes to prevent core dumps and make "database" engine work correctly.
18 * Revision 1.1 1998/07/21 11:36:21 ghn
21 * Copyright 1998 - Greg Nelson
22 * Redistributable under the terms of the GNU Public Licence (GPL)
32 #include <sys/types.h>
40 /* Some systems (e.g. Linux) implement strdup but don't define
41 it by default in string.h. Define it explicitly here, which
42 doesn't seem to do any harm on other systems. If you encounter
43 an error on the following declaration, disable it for your
46 /*extern char *strdup (const char *s1);*/
48 /* Eventually, these might deal with making a safe copy or mounting
49 and unmounting a partition. Until such time as we actually do
50 something with these, they are no-ops. */
54 #define DATAFMT "%Y%m/%%e"
55 #define PROJSTART 901947600L
57 static char *datafmt; /* Data file format string */
59 /* Prototypes for forward functions. */
61 static int32 FileLoadPacket(FILE *fp, EggCarton *cart);
62 static int32 next_filename(char *fn, uint32 tindex, int16 eggid,
63 int16 *lastind, int16 mustexist);
64 static int32 next_poss_fn(char *fn, uint32 tindex, int16 eggid);
66 /* The seek optimisation table saves the file name, last
67 packet returned, and corresponding file of the next
68 packet (if any) in the file, for the last SEEK_OPT_MAX
69 requests. This allows LoadNextPacket to avoid searching
70 the entire database for the next packet if the request
71 matches one in the seek optimisation table.
75 #define SEEK_OPT_MAX MAX_BASKETS
83 static struct seekopt seekOpt[SEEK_OPT_MAX];
84 static int seekOptIndex = 0;
86 /* The initialization provides a way to specify a format for the
87 database file names. If the argument is null, it uses a default
88 from the environment, or it takes the default given above. This
89 string is used as an argument to strftime(3) which replaces certain
90 characters with corresponding fields of the date and time. This
91 provides an automatic way of generating a logical new filename on a
92 periodic basis. Note that you can call InitStorage as many times
93 as you wish to change the format for file names opened
96 int32 InitStorage(char *path) {
101 /* First, take this opportunity to clear the seek optimisation
102 table. If we're changing the file name format, none of the
103 items in it will be valid in any case. */
105 memset(seekOpt, 0, sizeof seekOpt);
107 /* Set the path based on the environment variable, argument,
111 if ((path = getenv("EGG_DATA")) == NULL) {
116 /* If the path contains any of our special "$" editing
117 codes, interpolate the requested text into the string.
118 Strftime "%" editing codes are left intact. */
122 while ((np = strchr(p, '$')) != NULL) {
123 char *phrargs = np + 1;
125 /* Copy portion of string prior to phrase to output. */
130 memcpy(ns + l, p, np - p);
131 ns[l + (np - p)] = 0;
134 /* Parse format phrase and possible arguments. */
136 while ((*phrargs != 0) && !isalpha(*phrargs)) {
140 fprintf(stderr, "Data format string error in:\n %s\nunterminated $ phrase.\n", path);
144 /* Copy arguments, if any, to second character of arguments
145 string for possible use by phrase interpreters in
148 pa[0] = '%'; /* Start editing phrase */
150 if (phrargs > (np + 1)) {
151 memcpy(pa + 1, np + 1, phrargs - (np + 1));
152 pa[1 + (phrargs - (np + 1))] = 0;
154 /*fprintf(stderr, "Phrase arguments = <%s>\n", pa);*/
156 /* Now interpret the specific format phrase letters.
157 The value selected by each phrase should be concatenated
158 to the output string ns. Available format phrases
163 $[0][n]E Egg number, right justified,
164 optionally zero filled in n characters
171 case 'b': /* Basket name */
172 strcat(ns, baskettable[0].name);
175 case 'e': /* Egg name */
176 strcat(ns, eggtable[0].name);
179 case 'E': /* Egg number, edited decimal number */
181 sprintf(ns + strlen(ns), pa, eggtable[0].id);
185 fprintf(stderr, "Data format string error in:\n %s\nunknown phrase $%c.\n",
190 /* Adjust pointer to resume scan following the phrase
191 in the source string. */
198 strcat(ns, p); /* Concatenate balance of string */
201 fprintf(stderr, "Cannot allocate memory for file name format string.\n");
204 /*printf("Expanded path name phrase = <%s>\n", path); exit(0);*/
211 /* Save packet is the basic save function. It does not take a
212 filename, relying on the datafmt variable created during
213 initialization to generate an appropriate filename. The
214 date for the saved data is based on the timestamp of the packet's
217 int32 SavePacket(EggCarton *cart) {
219 char *packet, datatmp[255], datafile[255], *sp;
222 pktime = cart->records[0].timestamp;
224 time_t pktime_val = pktime;
225 fprintf(stderr, "SavePacket for %u: %s", pktime, asctime(gmtime(&pktime_val)));
228 /* Generate the file name corresponding to the date of
229 the first record in the packet. Since we know the
230 date, there's no need to use next_filename. */
232 res = next_poss_fn(datafile, pktime, cart->hdr.eggid);
234 /* Open the file for appending. */
237 fp = fopen(datafile, "a");
239 /* If we can't open the file, the odds are it's because
240 the file is in a directory we've yet to create. */
244 fprintf(stderr, "SavePacket: no such file %s\n", datafile);
246 sp = strrchr(datafile, '/');
248 strncpy(datatmp, datafile, sp - datafile);
249 datatmp[sp - datafile] = 0;
250 mkdir(datatmp, 0777);
252 fprintf(stderr, "SavePacket: mkdir %s\n", datatmp);
254 sp = strchr(sp + 1, '/');
257 /* Now try re-opening the file. */
260 if ((fp = fopen(datafile, "a")) == NULL) {
261 fprintf(stderr, "SavePacket: cannot create database file %s.\n", datafile);
268 fprintf(stderr, "SavePacket: error opening %s\n", datafile);
272 packet = Packetize(cart);
273 if (!packet) return -1;
274 fwrite(packet, sizeof(char), cart->hdr.pktsize, fp);
278 cart->hdr.numrec = 0;
283 /* Open database for reading, positioning to specified location.
284 If eggid is less than zero, it will load any packet. */
285 int32 OpenDatabase(DBRec *dbp, uint32 tindex, int16 eggid) {
289 time_t tindex_val = tindex;
290 fprintf(stderr, "OpenDatabase: Egg = %d, tindex = %u %s",
291 eggid, tindex, asctime(gmtime(&tindex_val)));
295 if ((res = next_filename(dbp->fn, tindex, eggid, &(dbp->eggind), TRUE)) < 0)
299 dbp->fp = fopen(dbp->fn, "r");
300 if (dbp->fp == NULL) {
303 fprintf(stderr, "OpenDatabase -- EOF\n");
308 fprintf(stderr, "OpenDatabase -- Opened %s\n", dbp->fn);
315 int32 CloseDatabase(DBRec *dbp) {
318 fprintf(stderr, "CloseDatabase -- Closed %s\n", dbp->fn);
322 /* We preserve file name; this lets us not open the same
323 file name again unless we want to. */
327 /* Reset database allows us to open same file name again. */
328 int32 ResetDatabase(DBRec *dbp) {
331 fprintf(stderr, "ResetDatabase\n");
336 /* Load next packet fitting specified time index and egg id.
337 If eggid is less than zero, it will load any packet. */
339 static int32 LoadNextPacket(DBRec *dbp, uint32 tindex, int16 eggid, EggCarton *cart) {
342 uint32 findex = tindex, now;
344 now = getzulutime(NULL);
345 if (findex < PROJSTART) {
349 fprintf(stderr, "LoadNextPacket(%s, %u, %d)\n", dbp->fn, tindex, eggid);
352 /* See if the start address for this request is present
353 in the seek optimisation table. If so, seek directly
354 to the address to avoid having to read through all
355 earlier packets in the database. */
358 /* Conditioning while on tindex > 0 causes seek optimisation to be
359 skipped when a request for any packet is received. */
363 i = SEEK_OPT_MAX - 1;
365 if (strcmp(seekOpt[i].filename, dbp->fn) == 0 &&
366 seekOpt[i].last_time == tindex) {
367 fseek(dbp->fp, seekOpt[i].next_packet, SEEK_SET);
369 time_t last_time_val = seekOpt[i].last_time;
370 fprintf(stderr, "LoadNextPacket; Seek optimised [%d] to %d for\n file %s at %u %s",
372 seekOpt[i].next_packet,
374 seekOpt[i].last_time,
375 asctime(gmtime(&last_time_val)));
380 if (i == seekOptIndex) {
382 time_t tindex_val = tindex;
383 fprintf(stderr, "LoadNextPacket; Cannot optimise seek in file %s\n at %u %s",
384 dbp->fn, tindex, asctime(gmtime(&tindex_val)));
386 break; /* Search wrapped table--cannot optimise */
391 res = FileLoadPacket(dbp->fp, &pktbuf);
392 if (res == ERR_EOF) {
393 /* File is finished. Close and open the next. We assume
394 that tindex and eggid have been tracking appropriately. */
397 /* Advance findex to start of next day after the one we've
400 #define SECONDS_PER_DAY (24L * 60 * 60)
401 findex = ((findex / SECONDS_PER_DAY) + 1) * SECONDS_PER_DAY;
403 time_t findex_val = findex;
404 fprintf(stderr, "LoadNextPacket; EOF, CloseDatabase, findex = %u: %s",
405 findex, asctime(gmtime(&findex_val)));
409 time_t now_val = now;
410 fprintf(stderr, "LoadNextPacket; EOF findex = %u > now = %u %s",
411 findex, now, asctime(gmtime(&now_val)));
415 /* Do not reset database; we won't open same one again. */
416 if ((res = OpenDatabase(dbp, findex, eggid)) < 0) return res;
420 fprintf(stderr, "Eggid = %d Packet.eggid = %d\n", eggid, pktbuf.hdr.eggid);
422 if (eggid >= 0 && pktbuf.hdr.eggid != eggid) continue;
423 for (i = 0; i < pktbuf.hdr.numrec; i++) {
424 /*fprintf(stderr, " Rec %ld timestamp = %lu tindex = %lu\n", i, pktbuf.records[i].timestamp, tindex);
426 if (pktbuf.records[i].timestamp > tindex) {
427 memcpy(cart, &pktbuf, sizeof(EggCarton));
429 /* Save the location of the packet following this one
430 in the seek optimisation table. */
432 strcpy(seekOpt[seekOptIndex].filename, dbp->fn);
433 seekOpt[seekOptIndex].last_time = pktbuf.records[pktbuf.hdr.numrec - 1].timestamp;
434 seekOpt[seekOptIndex].next_packet = ftell(dbp->fp);
436 time_t last_time_val = seekOpt[seekOptIndex].last_time;
437 fprintf(stderr, "LoadNextPacket; Seek optimisation[%d] for %s\n to address %d for time %u %s",
439 seekOpt[seekOptIndex].filename,
440 seekOpt[seekOptIndex].next_packet,
441 seekOpt[seekOptIndex].last_time,
442 asctime(gmtime(&last_time_val)));
444 seekOptIndex = (seekOptIndex + 1) % SEEK_OPT_MAX;
451 /* Load next packet following a particular time index and egg id */
453 int32 LoadPacket(uint32 tindex, int16 eggid, EggCarton *cart) {
457 ResetDatabase(&db); /* Here we need to be able to open same db as
459 if ((res = OpenDatabase(&db, tindex, eggid)) < 0) return res;
460 if ((res = LoadNextPacket(&db, tindex, eggid, cart)) < 0) return res;
461 return CloseDatabase(&db);
464 static int32 FileLoadPacket(FILE *fp, EggCarton *cart) {
469 if (feof(fp)) return ERR_EOF;
471 if (fread(&(cart->hdr.type), sizeof(uint16), 1, fp) != 1) {
472 if (feof(fp)) return ERR_EOF; else return ERR_CNREAD;
474 if (fread(&(cart->hdr.pktsize), sizeof(uint16), 1, fp) != 1) {
475 if (feof(fp)) return ERR_EOF; else return ERR_CNREAD;
478 /* Since the file packet is in network byte order, we need to
479 be sure the length field is in host order before using
480 it to allocate and read the balance of the packet from the
481 file. Once that's done, Unpacketize will take responsibility
482 for getting all the packet fields into host order. */
483 pksize = ntohs(cart->hdr.pktsize);
485 rbuf = (char *)malloc(pksize);
486 if (!rbuf) return ERR_NOMEM;
488 memcpy(rbuf, &(cart->hdr), 2*sizeof(uint16));
489 if (fread(rbuf+4, pksize - 4, 1, fp) != 1) return ERR_CNREAD;
491 res = Unpacketize(cart, rbuf);
497 static int32 next_filename(char *fn, uint32 tindex, int16 eggid,
498 int16 *lastind, int16 mustexist) {
505 time_t tindex_val = tindex;
506 fprintf(stderr, "next_filename: egg = %d, mustexist = %d, tindex = %u %s",
507 eggid, mustexist, tindex, asctime(gmtime(&tindex_val)));
509 if (eggid < 0 && !mustexist) {
511 fprintf(stderr, "next_filename: egg ID out of range.\n");
516 /* Can't go back before project began. */
517 now = getzulutime(NULL);
518 if (tindex < PROJSTART) {
524 /* Search possible file names until one is found. We jump by days,
525 assuming files don't have a greater granularity than that. In
526 the existence-testing process, we are checking to make sure the
527 file name is different each time. */
529 if (lastind == NULL) eggid = eggtable[0].id;
531 if (eggid < 0) eggid = eggtable[(*lastind)++].id;
532 if (*lastind == numeggs) *lastind = 0;
537 res = next_poss_fn(fn, findex, eggid);
539 if (!strcmp(fn, ldf)) continue;
541 /* Check for file. If stat fails, reason is ENOENT, isn't it? */
542 if (stat(fn, &statbuf) >= 0) {
544 fprintf(stderr, "next_filename: Found file %s\n", fn);
550 fprintf(stderr, "next_filename: File %s does not exist.\n", fn);
554 } while (findex < now+86400L);
557 fprintf(stderr, "next_filename: End of file.\n");
562 /* tindex and eggid must be defined here */
563 static int32 next_poss_fn(char *fn, uint32 tindex, int16 eggid) {
564 char datatmp[255], *sp;
568 time_t time_index = tindex;
569 tm = gmtime(&time_index);
570 strftime(datatmp, 255, datafmt, tm);
572 fprintf(stderr, "next_poss_fn: egg = %d, tindex = %u %s",
573 eggid, tindex, asctime(tm));
576 /* If an %e phrase remains after processing by strftime
577 (it should be specified as %%e in the original
578 DATAFMT specification), interpolate the egg number
581 if ((sp = strchr(datatmp, '%')) != NULL) {
584 eggind < numeggs && eggtable[eggind].id != eggid;
586 if (eggind == numeggs) {
588 fprintf(stderr, "next_poss_fn: Egg number out of range.\n");
594 strcat(fn, eggtable[eggind].name);
597 fprintf(stderr, "Bad file format expr: %%%c\n", sp[1]);
605 fprintf(stderr, "next_poss_fn: File name = %s\n", fn);