home *** CD-ROM | disk | FTP | other *** search
/ Usenet 1994 October / usenetsourcesnewsgroupsinfomagicoctober1994disk2.iso / unix / volume19 / rkive / part04 / news_arc.c next >
C/C++ Source or Header  |  1989-06-29  |  20KB  |  760 lines

  1. /*
  2. **
  3. ** This software is Copyright (c) 1989 by Kent Landfield.
  4. **
  5. ** Permission is hereby granted to copy, distribute or otherwise 
  6. ** use any part of this package as long as you do not try to make 
  7. ** money from it or pretend that you wrote it.  This copyright 
  8. ** notice must be maintained in any copy made.
  9. **
  10. **
  11. **  History:
  12. **    Creation: Tue Feb 21 08:52:35 CST 1989 due to necessity.
  13. **                                                               
  14. */
  15. #ifndef lint
  16. static char SID[] = "@(#)news_arc.c    1.1 6/1/89";
  17. #endif
  18.  
  19. #include <sys/types.h>
  20. #include <sys/stat.h>
  21. #include <dirent.h>
  22. #include <stdio.h>
  23. #include <ctype.h>
  24. #include "article.h"
  25. #include "cfg.h"
  26.  
  27. /*
  28. ** Defines for the type of "problems"
  29. ** encountered in saving the articles.
  30. */
  31. #define DUP_PROB    0
  32. #define NAME_PROB   1
  33. #define VOL_PROB    2
  34. #define TYPE_PROB   3
  35.  
  36. int test = 0;
  37. int problem_article;
  38.  
  39. extern struct group_archive *newsgrp;
  40. extern int overwrite;
  41.  
  42. char *strchr();
  43. char *strcpy();
  44. char *strcat();
  45. char *do_problem();
  46. char *basename();
  47. char *suffix();
  48. FILE *efopen();
  49. void exit();
  50.  
  51. get_header(filename)
  52.     char *filename;
  53. {
  54.     char *dp;
  55.     int header_ok = 0;
  56.     FILE *gfp;
  57.  
  58.     init_article();
  59.  
  60.     gfp = efopen(filename,"r");
  61.  
  62.     (void) strcpy(article.newsarticle, filename);
  63.  
  64.     while (fgets(s,sizeof s,gfp) != NULL) {
  65.         if (debug)
  66.             (void) fprintf(logfp, "BUF = [%s]",s);
  67.  
  68.         if (!isalpha(*s) || (strchr(s,':') == NULL)) {
  69.            header_ok++;
  70.            if (header_ok == 2) 
  71.                break;
  72.            continue;
  73.         }
  74.  
  75.         dp = s;
  76.         while (*++dp)
  77.            if (*dp == '\n')
  78.                *dp = '\0';
  79.  
  80.         store_line();
  81.     }
  82.     (void) fclose(gfp);
  83.  
  84.     if (debug)
  85.         dump_article();
  86. }
  87.  
  88. /*
  89. ** check_archive_name
  90. **
  91. ** Assure the path specified is within the base directory
  92. ** specified by the archive administrator by assuring that
  93. ** a prankster could not have an article archived at a
  94. **     basedir/../../../etc/passwd
  95. ** location.
  96. **
  97. ** If an absoulte path is specified in the Archive-name, it
  98. ** is of no concern since a "checked" base directory and
  99. ** volume directory are prefixed.
  100. */
  101.  
  102. check_archive_name(argstr)
  103.     char *argstr;
  104.  {
  105.     char *substr();
  106.     register char *rp;
  107.     register char *dp;
  108.  
  109.     /* 
  110.     ** check to assure that the path specified
  111.     ** does not contain the '..' sequence.
  112.     */
  113.  
  114.     while ((rp = substr(argstr, "..")) != NULL) {
  115.        dp = rp+2;
  116.        while(*dp)
  117.            *rp++ = *dp++;
  118.        *rp = '\0';
  119.     }
  120.  
  121.     /* I know this is not necessary but what the heck.. */
  122.  
  123.     while ((rp = substr(argstr, "//")) != NULL) {
  124.        dp = rp+2;
  125.        ++rp;
  126.        while(*dp)
  127.            *rp++ = *dp++;
  128.        *rp = '\0';
  129.     }
  130.  
  131.     /* 
  132.     ** strip the string of trailing '/'s
  133.     */
  134.  
  135.     dp = argstr+(strlen(argstr)-1);
  136.     while(*dp == '/' && dp > argstr)
  137.         *dp = '\0';
  138. }
  139.  
  140. /*
  141. ** IF YOU USE A COMPRESSION ROUTINE OTHER THAN COMPRESS
  142. ** OR PACK, ADD YOUR COMPRESSION SPECIFIC INFORMATION
  143. ** TO THE cprgs COMPRESS_TABLE ......
  144. */
  145.  
  146. struct compress_tab {
  147.     char     *com_name;
  148.     char     *com_suffix;
  149. };
  150.  
  151. struct compress_tab cprgs[] = {
  152. {    "compress",        ".Z"    },
  153. {    "pack",            ".z"    },
  154. {    NULL,            0    },
  155. };
  156.  
  157. char *suffix(compression)
  158.     char *compression;
  159.  {
  160.     struct compress_tab *ct;
  161.  
  162.     ct = &cprgs[0];
  163.     while ((ct->com_name) != NULL) {
  164.         if (strcmp(compression, ct->com_name) == 0) 
  165.             return(ct->com_suffix);
  166.         ct++;
  167.     }
  168.     return("");
  169. }
  170.  
  171. int remove_suffix(path_str)
  172. char *path_str;
  173.  {
  174.     char *ss;
  175.     struct compress_tab *ct;
  176.  
  177.     /*
  178.     ** need to compare the filename passed in to 
  179.     ** the compression suffix table in order to
  180.     ** determine if the file has a recognized,
  181.     ** compression suffix attached.
  182.     */
  183.     
  184.     ss = path_str + (strlen(path_str) -2);
  185.  
  186.     ct = &cprgs[0];
  187.     while ((ct->com_name) != NULL) {
  188.         if (strcmp(ss, ct->com_suffix) == 0) {
  189.             *ss = '\0';
  190.             return(TRUE);
  191.         }
  192.         ct++;
  193.     }
  194.     return(FALSE);
  195. }
  196.  
  197. char *expand_name(filename,ng)
  198. char *filename;
  199. struct group_archive *ng;
  200. {
  201.     char *comp_cmd;
  202.     static char compress_path[MAXNAMLEN];
  203.  
  204.     (void) strcpy(compress_path, filename);
  205.  
  206.     /*
  207.     ** Check to see if a group specific compress was specified.      
  208.     ** If so, then attach the suffix and return.                    
  209.     ** Else check to see if a global compress was specified. If so,
  210.     ** then attach the suffix and return.                         
  211.     ** If both are NULL, return filename.                        
  212.     */
  213.  
  214.     if (*(ng->compress)) {
  215.         comp_cmd = basename(ng->compress);
  216.         (void) strcat(compress_path, suffix(comp_cmd));
  217.     }
  218.     else if (*compress) {
  219.         comp_cmd = basename(compress);
  220.         (void) strcat(compress_path, suffix(comp_cmd));
  221.     }
  222.     return(compress_path);
  223. }
  224.  
  225. #ifdef REDUCE_HEADERS
  226.  
  227. struct hdrstokeep {
  228.     char     *ststr;
  229.     int    stbytes;
  230. };
  231.  
  232. struct hdrstokeep hdrs[] = {
  233. {    "From:",        (sizeof "From:")    },
  234. {    "Newsgroups:",        (sizeof "Newsgroups:")    },
  235. {    "Subject:",        (sizeof "Subject:")    },
  236. {    "Message-ID:",        (sizeof "Message-ID:")    },
  237. {    "Date:",        (sizeof "Date:")    },
  238. {    NULL,            0            },
  239. };
  240.  
  241. int keep_line(argstr)
  242.     char *argstr;
  243.  {
  244.     struct hdrstokeep *pt;
  245.  
  246.     pt = &hdrs[0];
  247.     while ((pt->ststr) != NULL) {
  248.         if (strncmp(argstr, pt->ststr, (pt->stbytes-1)) == 0) 
  249.             return(TRUE);
  250.         pt++;
  251.     }
  252.     return(FALSE);
  253. }
  254.  
  255. int copy(source, target)
  256.     char *source, *target;
  257. {
  258.     char *strchr();
  259.     FILE *from, *to;
  260.     char fbuf[BUFSIZ];
  261.     int inheader;
  262.  
  263.     inheader = TRUE;  
  264.  
  265.     if (verbose) {
  266.         (void) fprintf(logfp,"archive <%s> to <%s>\n",source,target);
  267.         if (test) 
  268.             return(0);
  269.     }
  270.     if ((from = fopen(source, "r")) == NULL) {
  271.         (void) fprintf(errfp,"%s: cannot open %s\n",progname,source);
  272.         return (-1);
  273.     }
  274.     if ((to = fopen(target, "w")) == NULL) {
  275.         (void) fclose(from);
  276.         (void) fprintf(errfp,"%s: cannot create %s\n",progname,target);
  277.         return (-1);
  278.     }
  279.     /*
  280.     ** Read the source and do not print any headers 
  281.     ** unless specified in the "keep" headers table.
  282.     */
  283.  
  284.     while (fgets(fbuf, BUFSIZ, from) != NULL) {
  285.         if (inheader) {
  286.             /* 
  287.             ** Have I encountered a line without a line type ? 
  288.             */
  289.             if (!isalpha(*fbuf) || (strchr(fbuf,':') == NULL)) 
  290.                 inheader = FALSE;
  291.  
  292.             else {
  293.                 /*
  294.                 ** Determine the type of the header line and 
  295.                 ** decide if this is a line to be kept or pitched.
  296.                 */
  297.                 if (!keep_line(fbuf))
  298.                     continue;
  299.             }
  300.         }
  301.         if (fputs(fbuf, to) == EOF) {
  302.             (void) unlink(target);
  303.             (void) fclose(from);
  304.             (void) fclose(to);
  305.             (void) fprintf(errfp,"%s: bad copy to %s\n",progname,target);
  306.             return (-1);
  307.         }
  308.     }
  309.     (void) fclose(from);
  310.     (void) fclose(to);
  311.     return(0);
  312. }
  313.  
  314. #else
  315.  
  316. copy(source, target)
  317.     char *source, *target;
  318. {
  319.     int from, to, ct;
  320.     char fbuf[BUFSIZ];
  321.  
  322.     if (verbose) {
  323.         (void) fprintf(logfp,"archive <%s> to <%s>\n",source,target);
  324.         if (test) 
  325.             return(0);
  326.     }
  327.     if ((from = open(source, 0)) < 0) {
  328.         (void) fprintf(errfp,"%s: cannot open %s\n",progname,source);
  329.         return (-1);
  330.     }
  331.     if ((to = creat (target, 0644)) < 0) {
  332.         (void) close(from);
  333.         (void) fprintf(errfp,"%s: cannot create %s\n",progname,target);
  334.         return (-1);
  335.     }
  336.     while ((ct = read(from, fbuf, BUFSIZ)) != 0) {
  337.         if(ct < 0 || write(to, fbuf, (unsigned) ct) != ct) {
  338.             (void) unlink(target);
  339.             (void) close(from);
  340.             (void) close(to);
  341.             (void) fprintf(errfp,"%s: bad copy to %s\n",progname,target);
  342.             return (-1);
  343.         }
  344.     }
  345.     (void) close(from);
  346.     (void) close(to);
  347.     return(0);
  348. }
  349.  
  350. #endif /* REDUCE_HEADERS */
  351.  
  352. /*
  353. ** mkparents:
  354. **
  355. ** If any parent directories in 
  356. ** fullname don't exist, create them.
  357. */
  358.  
  359. int mkparents(fullname)
  360. char *fullname;
  361. {
  362.     char *strrchr();
  363.  
  364.     register char *p;
  365.     char b[MAXNAMLEN];
  366.     int rc;
  367.  
  368.     (void) strcpy(b, fullname);
  369.  
  370.     if ((p = strrchr(b, '/')) != NULL) 
  371.             *p = '\0';
  372.     else                  /* no directories in fullname */
  373.        return(0);
  374.  
  375.     if (*b == '\0')           /* are we at the root ? */
  376.         return(0);
  377.  
  378.     if (access(b, 0) == 0)
  379.         return(0);
  380.  
  381.     (void) mkparents(b);
  382.  
  383.     if ((rc = makedir(b, 0755, newsgrp->owner, newsgrp->group)) != 0) 
  384.         error("makedir failed attempting to make", b);
  385.  
  386.     return(rc);
  387. }
  388.  
  389.  
  390. char *save_article (filename,ng)
  391. char *filename;
  392. struct group_archive *ng;
  393. {
  394.     char *final_path;
  395.     static char path[MAXNAMLEN];
  396.     struct stat sb;
  397.  
  398.     problem_article = FALSE;
  399.     path[0] = '\0';
  400.  
  401.     /*
  402.     ** Read the news article file to extract the
  403.     ** header information and fill appropriate
  404.     ** data structures.
  405.     */
  406.     get_header(filename);
  407.  
  408.     /*
  409.     ** Build the path string for the final resting spot
  410.     ** for the new archive member.
  411.     */
  412.     switch(ng->type) {
  413.     case ARCHIVE_NAME:
  414.             /*
  415.             ** The header's archive_name contains the filename in
  416.             ** an "elm/part06" format.
  417.             */
  418.  
  419.             if ((article.volume == -1) || (!header.archive_name[0])) 
  420.                 return(do_problem(NAME_PROB, ng,filename,path));
  421.     
  422.             /*
  423.             ** Assure the address is relative and
  424.             ** that some prankster can not do nasty
  425.             ** things to your system files by having
  426.             ** an Archive-name line like:
  427.             **    ../../../../../etc/passwd
  428.             */
  429.  
  430.             check_archive_name(header.archive_name);
  431.  
  432.             /* 
  433.             ** Check to see if the article is a patch. If so,
  434.             ** check to see if the administrator wishes to
  435.             ** store the patch with the initially posted
  436.             ** articles. This really relys on the archive name
  437.             ** being correct.
  438.             */
  439.             
  440.             if (article.rectype == PATCH && ng->patch_type == PACKAGE)
  441.                 /*
  442.                 ** Store the patch in the volume specified with the
  443.                 ** Archive-name: specified file name.
  444.                 */
  445.                 (void) sprintf(path,"%s/%s%d/%s", ng->location, VOLUME,
  446.                     article.patch_volume, header.archive_name);
  447.  
  448.             else 
  449.                 (void) sprintf(path,"%s/%s%d/%s", ng->location, VOLUME,
  450.                 article.volume, header.archive_name);
  451.             break;
  452.     case VOLUME_ISSUE:
  453.             /*
  454.             ** The article filename contains the filename in
  455.             ** a "v01i001" format.
  456.             */
  457.             if ((article.volume == -1) || (!article.filename[0])) 
  458.                 return(do_problem(VOL_PROB,ng,filename,path));
  459.  
  460.             (void) sprintf(path,"%s/%s%d/%s", ng->location, VOLUME,
  461.                 article.volume, article.filename);
  462.             break;
  463.     case ARTICLE_NUMBER:
  464.             /*
  465.             ** Store in same filename - thanks news...
  466.             */
  467.             (void) sprintf(path,"%s/%s", ng->location, filename);
  468.             break;
  469.     default:
  470.             /*
  471.             ** We have got problems....
  472.             */
  473.             return(do_problem(TYPE_PROB,ng,filename,path));
  474.     }
  475.  
  476.     /*
  477.     ** Check if the file is a patch. If so, log
  478.     ** the patch information into the patch log
  479.     ** in a *non-configurable* format so that
  480.     ** applications can be written to access the
  481.     ** file's "known format".
  482.     */
  483.  
  484.     if (article.rectype == PATCH)
  485.         write_patch_log(ng,path);
  486.  
  487. #ifdef ADD_REPOST_SUFFIX
  488.     if (article.repost == TRUE)
  489.         /*
  490.         ** The ADD_REPOST_SUFFIX code adds the REPOST_SUFFIX
  491.     ** to any file that has been indicated as a repost
  492.     ** by the moderator. This should not be used with 
  493.     ** Archive-Name archiving on a filesystem with 14
  494.     ** character filename limits or filename truncation
  495.     ** can occur. You have been warned... :-(
  496.     **
  497.      ** After adding the REPOST_SUFFIX, the filename is
  498.     ** treated as any other file with the duplication
  499.     ** checks and all...
  500.     */
  501.     (void) strcat(path,REPOST_SUFFIX);
  502. #endif /* ADD_REPOST_SUFFIX */
  503.  
  504.     /* 
  505.     ** expand the path to the file to include the 
  506.     ** compression suffix if necessary.
  507.     */
  508.  
  509.     final_path = expand_name(path, ng);
  510.  
  511.     /*
  512.     ** Make any necessary directories 
  513.     ** along the way. 
  514.     */
  515.     (void) mkparents(path);
  516.  
  517.     /*
  518.     ** Check to assure that there is not already 
  519.     ** a file with the same file name. If so
  520.     ** copy (or archive) the file to the problems 
  521.     ** directory. 
  522.     **
  523.     ** This works for REPOSTS as well.
  524.     ** If the REPOST arrives and there is
  525.     ** no file currently at the archive location, the
  526.     ** REPOST is installed in the correct archive 
  527.     ** location.
  528.     ** If there is a file that exists when a REPOST
  529.     ** arrives, the REPOST is then handled in do_problem().
  530.     */
  531.  
  532.     if ((stat(final_path ,&sb) == 0) && !overwrite)  /* duplicate found */
  533.         return(do_problem(DUP_PROB,ng, filename, final_path));
  534.  
  535.     if (copy(filename,path) != 0) {  
  536.         (void) fprintf(errfp,"copy failed for %s to %s\n",filename,path);
  537.         return(NULL);
  538.     }  
  539.     /* 
  540.     ** Write the filename to the .archived file in the newsgroup's
  541.     ** BASEDIR directory since we do not want it rearchived tomorrow.
  542.     */
  543.     write_archived(filename, path);
  544.  
  545.     /*
  546.     ** Return the path to the archived file.
  547.     */
  548.     return(path);
  549. }
  550.  
  551.  
  552.  
  553. char *do_problem(type_of_problem, ng, file, path)
  554. int type_of_problem;
  555. struct group_archive *ng;
  556. char *file;
  557. char *path;
  558. {
  559.  
  560. #ifdef MV_ORIGINAL
  561.     char crnt_path[MAXNAMLEN];
  562. #endif /*MV_ORIGINAL */
  563.  
  564.     char pmess[BUFSIZ];
  565.  
  566.     problem_article = TRUE;
  567.  
  568.     /* ALERT THE ADMINISTRATOR THAT A PROBLEM WAS ENCOUNTERED 
  569.     **
  570.     ** A problem has been encountered. It could be that there is an
  571.     ** format mismatch or there is already a file with the same 
  572.     ** issue/archive/msg-id name.
  573.     ** Copy the problem file to the problems directory. 
  574.     ** Alert the Administrator that a problem was received.
  575.     */
  576.  
  577.     (void) sprintf(pmess,"PROBLEM: Article %s in %s ",file,ng->ng_name);
  578.  
  579.     switch( type_of_problem ) {
  580.        case NAME_PROB:
  581.           (void) strcat(pmess,"does not support Archive-Name Archiving\n.");
  582.           break;
  583.        case VOL_PROB:
  584.           (void) strcat(pmess,"does not support Volume-Issue Archiving\n.");
  585.           break;
  586.        case TYPE_PROB:
  587.           (void) strcat(pmess,"has an invalid archive TYPE specified\n.");
  588.           break;
  589.        case DUP_PROB:
  590.           if (article.repost != TRUE) 
  591.               (void) strcat(pmess,"is a Duplicate article.\n");
  592.           else 
  593.              (void) strcat(pmess,"is a Reposted article.\n");
  594.           (void) sprintf(pmess,"%s\tExisting Archived path - %s", pmess,path);
  595.           break;
  596.     }
  597.  
  598.     /* print the message out to the screen, crontab output, etc */
  599.  
  600.     (void) fprintf(errfp,"%s\n",pmess);
  601.  
  602.     /* log the initial detection message. */
  603.  
  604.     record_problem(pmess, file, ng);
  605.  
  606.     /* Handling Repostings.
  607.     **
  608.     ** MV_ORIGINAL
  609.     **     The original article is placed into a "original" directory in 
  610.     **     the problems directory (if duplicated). The inbound reposted
  611.     **     article is placed into the archive in the correct position.
  612.     **
  613.     ** ADD_REPOST_SUFFIX 
  614.     **     If ADD_REPOST_SUFFIX is defined, all reposts will have the 
  615.     **     string specified in REPOST_SUFFIX appended to the archive
  616.     **     filename so that a repost of elm/part07 would appear in
  617.     **     the archive as elm/part07-repost prior to any compression.
  618.     **     The addition of the suffix was done in save_article().
  619.     **     Handle this as the true duplicated article that it is.
  620.     **
  621.     ** No Reposting Defines specified:
  622.     **    The inbound article would be placed into the archive in the 
  623.     **    correct position only if the initial article is not in the archive.
  624.     **    Otherwise the reposted article is placed in the problems directory 
  625.     **    as a normal duplicate article as it is now.
  626.     */
  627.  
  628. #ifdef MV_ORIGINAL
  629.     if (article.repost == TRUE) {
  630.         /*
  631.         ** save the duplicated path 
  632.         ** Caution: may have compression suffix attached
  633.         */
  634.         (void) strcpy(crnt_path, path);
  635.  
  636.         /* create the storage path for original copy */
  637.         /* no slash needed between Originals and crnt_path below.. */
  638.  
  639.         (void) sprintf(path,"%s/%s%s",problems_dir,"Originals",crnt_path);
  640.  
  641.         /* Display and record the actions */ 
  642.         (void) sprintf(pmess,"\tMoving %s (original)\n\tto %s",crnt_path,path);
  643.         (void) fprintf(errfp,"%s\n",pmess);
  644.         record_problem(pmess, file, ng);
  645.  
  646.         /* Make any necessary directories along the way. */
  647.         (void) mkparents(path);
  648.  
  649.         /* copy the original out of the way */
  650.         if (copy(crnt_path,path) != 0) {
  651.             (void) fprintf(errfp,"copy failed for %s to %s\n", crnt_path, path);
  652.             return(NULL);
  653.         }
  654.  
  655.         set_ownership(path, ng);
  656.  
  657.         /* restore the destination path for inbound article */
  658.         (void) strcpy(path,crnt_path);
  659.  
  660.         /* remove the existing file */
  661.         (void) unlink(path);
  662.         /*
  663.         ** Must assure that "path" does not have a .Z type
  664.         ** of suffix used in compression. If it does, it must 
  665.     ** be removed before continuing. This is cheating and
  666.         ** will probably break but what the hell.
  667.         */
  668.         (void) remove_suffix(path);
  669.     }
  670.     else 
  671.  
  672. #endif /* MV_ORIGINAL */
  673.  
  674.     /*
  675.     ** Build the path string for the location of the article in 
  676.     ** the problems directory. Place the file in the appropriate 
  677.     ** directory in Article-Number format. In this manner, multiple 
  678.     ** problems will be stored as separate files. 
  679.     */
  680.  
  681.         (void) sprintf(path,"%s/%s/%s",problems_dir,ng->ng_name,file);
  682.  
  683.     /* Display and record the actions */ 
  684.     (void) sprintf(pmess,"\tStoring Article %s at %s\n", file, path);
  685.     (void) fprintf(errfp,"%s\n",pmess);
  686.     record_problem(pmess, file, ng);
  687.  
  688.     /* Make any necessary directories along the way. */
  689.     (void) mkparents(path);
  690.  
  691.     if (copy(file,path) != 0) {  
  692.         (void) fprintf(errfp,"copy failed for %s to %s\n", file, path);
  693.         return(NULL);
  694.     }  
  695.  
  696.     /* 
  697.     ** Write the filename to the .archived file in the newsgroup's
  698.     ** BASEDIR directory since we do not want it rearchived tomorrow.
  699.     */
  700.     write_archived(file, path);
  701.  
  702.     /*
  703.     ** Return the path to the stored problem file.
  704.     */
  705.     return(path);
  706. }
  707.  
  708. write_patch_log(ng, path)
  709.     struct group_archive *ng;
  710.     char *path;
  711. {
  712.         char *sp;
  713.     FILE *plfp;
  714.     struct stat sb;
  715.         int hn;
  716.  
  717.     /* 
  718.     ** The .patchlog file is used to record the
  719.     ** information specific to patches that come
  720.     ** through the newsgroup.
  721.     **
  722.     ** The format of the .patchlog file is:
  723.     **
  724.     ** path-to-patch  initial-volume  initial-issue  volume issue 
  725.     ** bb/patch01          22              105         23    77
  726.     ** v47i022             22              105         23    77
  727.     */
  728.  
  729.         /*
  730.         ** If this is the first time that an entry is written to the
  731.         ** patch log, add a header on top of the file for informational
  732.         ** purposes only...
  733.         */
  734.     if ((stat(ng->patchlog ,&sb) != 0)) {
  735.         plfp = efopen(ng->patchlog,"a+");
  736.  
  737.         (void) fprintf(plfp,"#\n#\tPatch log for %s\n#\n",
  738.                     ng->ng_name);
  739.  
  740.         (void) fprintf(plfp,"# %-30s%-11s%-13s%-6s%10s\n", 
  741.                     "Path To", "Initial", "Initial",
  742.                     "Current", "Current");
  743.  
  744.         (void) fprintf(plfp,"# %-30s%-11s%6s%13s%10s\n#\n", 
  745.                     "Patchfile", "Volume", "Issue", "Volume", "Issue");
  746.         (void) fclose(plfp);
  747.         }
  748.  
  749.         /* 
  750.         ** Get rid of the base directory.
  751.         */
  752.         sp = path + (strlen(ng->location)+1);
  753.  
  754.     plfp = efopen(ng->patchlog,"a+");
  755.     (void) fprintf(plfp,"%-24s%12d%12d%12d%11d\n", sp,
  756.             article.patch_volume, article.patch_issue,
  757.             article.volume, article.issue);
  758.     (void) fclose(plfp);
  759. }
  760.