1 /*
2 **  Read batchfiles on standard input and archive them.
3 */
4 
5 #include "portable/system.h"
6 
7 #include <errno.h>
8 #include <sys/stat.h>
9 #include <time.h>
10 
11 #include "inn/buffer.h"
12 #include "inn/innconf.h"
13 #include "inn/libinn.h"
14 #include "inn/messages.h"
15 #include "inn/paths.h"
16 #include "inn/qio.h"
17 #include "inn/storage.h"
18 #include "inn/vector.h"
19 #include "inn/wire.h"
20 
21 
22 /* Holds various configuration options and command-line parameters. */
23 struct config {
24     const char *root;    /* Root of the archive. */
25     const char *pattern; /* Wildmat pattern of groups to process. */
26     FILE *index;         /* Where to put the index entries. */
27     bool concat;         /* Concatenate articles together. */
28     bool flat;           /* Use a flat directory structure. */
29 };
30 
31 
32 /*
33 **  Try to make one directory.  Return false on error.
34 */
35 static bool
MakeDir(char * Name)36 MakeDir(char *Name)
37 {
38     struct stat Sb;
39 
40     if (mkdir(Name, GROUPDIR_MODE) >= 0)
41         return true;
42 
43     /* See if it failed because it already exists. */
44     return stat(Name, &Sb) >= 0 && S_ISDIR(Sb.st_mode);
45 }
46 
47 
48 /*
49 **  Given an entry, comp/foo/bar/1123, create the directory and all
50 **  parent directories needed.  Return false on error.
51 */
52 static bool
mkpath(const char * file)53 mkpath(const char *file)
54 {
55     char *path, *delim;
56     bool status;
57 
58     path = xstrdup(file);
59     delim = strrchr(path, '/');
60     if (delim == NULL) {
61         free(path);
62         return false;
63     }
64     *delim = '\0';
65 
66     /* Optimize common case -- parent almost always exists. */
67     if (MakeDir(path)) {
68         free(path);
69         return true;
70     }
71 
72     /* Try to make each of comp and comp/foo in turn. */
73     for (delim = path; *delim != '\0'; delim++)
74         if (*delim == '/' && delim != path) {
75             *delim = '\0';
76             if (!MakeDir(path)) {
77                 free(path);
78                 return false;
79             }
80             *delim = '/';
81         }
82     status = MakeDir(path);
83     free(path);
84     return status;
85 }
86 
87 
88 /*
89 **  Write an article from memory into a file on disk.  Takes the handle of the
90 **  article, the file name into which to write it, and a flag saying whether
91 **  to concatenate the message to the end of an existing file if any.
92 */
93 static bool
write_article(ARTHANDLE * article,const char * file,bool concatenate)94 write_article(ARTHANDLE *article, const char *file, bool concatenate)
95 {
96     FILE *out;
97     char *text = NULL;
98     size_t length = 0;
99 
100     /* Open the output file. */
101     out = fopen(file, concatenate ? "a" : "w");
102     if (out == NULL && errno == ENOENT) {
103         if (!mkpath(file)) {
104             syswarn("cannot mkdir for %s", file);
105             return false;
106         }
107         out = fopen(file, concatenate ? "a" : "w");
108     }
109     if (out == NULL) {
110         syswarn("cannot open %s for writing", file);
111         return false;
112     }
113 
114     /* Get the data in wire format and write it out to the file. */
115     text = wire_to_native(article->data, article->len, &length);
116     if (concatenate)
117         fprintf(out, "-----------\n");
118     if (fwrite(text, length, 1, out) != 1) {
119         syswarn("cannot write to %s", file);
120         fclose(out);
121         if (!concatenate)
122             unlink(file);
123         free(text);
124         return false;
125     }
126     free(text);
127 
128     /* Flush and close the output. */
129     if (ferror(out) || fflush(out) == EOF) {
130         syswarn("cannot flush %s", file);
131         fclose(out);
132         if (!concatenate)
133             unlink(file);
134         return false;
135     }
136     if (fclose(out) == EOF) {
137         syswarn("cannot close %s", file);
138         if (!concatenate)
139             unlink(file);
140         return false;
141     }
142     return true;
143 }
144 
145 
146 /*
147 **  Link an article.  First try a hard link, then a soft link, and if both
148 **  fail, write the article out again to the new path.
149 */
150 static bool
link_article(const char * oldpath,const char * newpath,ARTHANDLE * art)151 link_article(const char *oldpath, const char *newpath, ARTHANDLE *art)
152 {
153     if (link(oldpath, newpath) < 0) {
154         if (!mkpath(newpath)) {
155             syswarn("cannot mkdir for %s", newpath);
156             return false;
157         }
158         if (link(oldpath, newpath) < 0)
159             if (symlink(oldpath, newpath) < 0)
160                 if (!write_article(art, newpath, false))
161                     return false;
162     }
163     return true;
164 }
165 
166 
167 /*
168 **  Write out a single header field to stdout, applying the standard overview
169 **  transformation to it.  This code is partly stolen from overdata.c; it
170 **  would be nice to find a way to only write this in one place.
171 */
172 static void
write_index_header(FILE * index,ARTHANDLE * art,const char * header)173 write_index_header(FILE *index, ARTHANDLE *art, const char *header)
174 {
175     const char *start, *end, *p;
176 
177     start = wire_findheader(art->data, art->len, header, false);
178     if (start == NULL) {
179         fprintf(index, "<none>");
180         return;
181     }
182     end = wire_endheader(start, art->data + art->len - 1);
183     if (end == NULL) {
184         fprintf(index, "<none>");
185         return;
186     }
187     for (p = start; p <= end; p++) {
188         if (*p == '\r' && p < end && p[1] == '\n') {
189             p++;
190             continue;
191         }
192         if (*p == '\0' || *p == '\t' || *p == '\r' || *p == '\n')
193             putc(' ', index);
194         else
195             putc(*p, index);
196     }
197 }
198 
199 
200 /*
201 **  Write an index entry to standard output.  This is the path (without the
202 **  archive root), the message-ID of the article, and the subject.
203 */
204 static void
write_index(FILE * index,ARTHANDLE * art,const char * file)205 write_index(FILE *index, ARTHANDLE *art, const char *file)
206 {
207     fprintf(index, "%s ", file);
208     write_index_header(index, art, "Subject");
209     putc(' ', index);
210     write_index_header(index, art, "Message-ID");
211     fprintf(index, "\n");
212     if (ferror(index) || fflush(index) == EOF)
213         syswarn("cannot write index for %s", file);
214 }
215 
216 
217 /*
218 **  Build the archive path for a particular article.  Takes a pointer to the
219 **  (nul-terminated) group name and a pointer to the article number as a
220 **  string, as well as the config struct.  Also takes a buffer to use to build
221 **  the path, which may be NULL to allocate a new buffer.  Returns the path to
222 **  which to write the article as a buffer (but still nul-terminated).
223 */
224 static struct buffer *
build_path(const char * group,const char * number,struct config * config,struct buffer * path)225 build_path(const char *group, const char *number, struct config *config,
226            struct buffer *path)
227 {
228     char *p;
229 
230     /* Initialize the path buffer to config-root followed by /. */
231     if (path == NULL)
232         path = buffer_new();
233     buffer_set(path, config->root, strlen(config->root));
234     buffer_append(path, "/", 1);
235 
236     /* Append the group name, replacing dots with slashes unless we're using a
237        flat structure. */
238     p = path->data + path->left;
239     buffer_append(path, group, strlen(group));
240     if (!config->flat)
241         for (; (size_t)(p - path->data) < path->left; p++)
242             if (*p == '.')
243                 *p = '/';
244 
245     /* If we're saving by date, append the date now.  Otherwise, append the
246        group number. */
247     if (config->concat) {
248         struct tm *tm;
249         time_t now;
250         int year, month;
251 
252         now = time(NULL);
253         tm = localtime(&now);
254         year = tm->tm_year + 1900;
255         month = tm->tm_mon + 1;
256         buffer_append_sprintf(path, "/%04d%02d", year, month);
257     } else {
258         buffer_append(path, "/", 1);
259         buffer_append(path, number, strlen(number));
260     }
261     buffer_append(path, "", 1);
262     return path;
263 }
264 
265 
266 /*
267 **  Process a single article, saving it to the appropriate file or files (if
268 **  crossposted).
269 */
270 static void
process_article(ARTHANDLE * art,const char * token,struct config * config)271 process_article(ARTHANDLE *art, const char *token, struct config *config)
272 {
273     char *start, *end, *xref, *delim, *p, *first;
274     const char *group;
275     size_t i;
276     struct cvector *groups;
277     struct buffer *path = NULL;
278 
279     /* Determine the groups from the Xref header field.  In groups will be the
280      * split Xref header field body; from the second string on should be a
281      * group, a colon, and an article number. */
282     start = wire_findheader(art->data, art->len, "Xref", true);
283     if (start == NULL) {
284         warn("cannot find Xref header field in %s", token);
285         return;
286     }
287     end = wire_endheader(start, art->data + art->len - 1);
288     xref = xstrndup(start, end - start);
289     for (p = xref; *p != '\0'; p++)
290         if (*p == '\r' || *p == '\n')
291             *p = ' ';
292     groups = cvector_split_space(xref, NULL);
293     if (groups->count < 2) {
294         warn("bogus Xref header field in %s", token);
295         free(xref);
296         return;
297     }
298 
299     /* Walk through each newsgroup, saving the article in the appropriate
300        location. */
301     first = NULL;
302     for (i = 1; i < groups->count; i++) {
303         group = groups->strings[i];
304         delim = strchr(group, ':');
305         if (delim == NULL) {
306             warn("bogus Xref entry %s in %s", group, token);
307             continue;
308         }
309         *delim = '\0';
310 
311         /* Skip newsgroups that don't match our pattern, if provided. */
312         if (config->pattern != NULL) {
313             if (uwildmat_poison(group, config->pattern) != UWILDMAT_MATCH)
314                 continue;
315         }
316 
317         /* Get the path to which to write the article. */
318         path = build_path(group, delim + 1, config, path);
319 
320         /* If this isn't the first group, and we're not saving by date, try to
321            just link or symlink between the archive directories rather than
322            writing out multiple copies. */
323         if (first == NULL || config->concat) {
324             if (!write_article(art, path->data, config->concat))
325                 continue;
326             if (groups->count > 2)
327                 first = xstrdup(path->data);
328         } else {
329             if (!link_article(first, path->data, art))
330                 continue;
331         }
332 
333         /* Write out the index if desired. */
334         if (config->index)
335             write_index(config->index, art,
336                         path->data + strlen(config->root) + 1);
337     }
338     free(xref);
339     cvector_free(groups);
340     if (path != NULL)
341         buffer_free(path);
342     if (first != NULL)
343         free(first);
344 }
345 
346 
347 int
main(int argc,char * argv[])348 main(int argc, char *argv[])
349 {
350     struct config config = {NULL, NULL, NULL, 0, 0};
351     int option, status;
352     bool redirect = true;
353     QIOSTATE *qp;
354     char *line, *file;
355     TOKEN token;
356     ARTHANDLE *art;
357     FILE *spool;
358     char buffer[BUFSIZ];
359 
360     /* First thing, set up our identity. */
361     message_program_name = "archive";
362 
363     /* Set defaults. */
364     if (!innconf_read(NULL))
365         exit(1);
366     config.root = innconf->patharchive;
367     umask(NEWSUMASK);
368 
369     /* Parse options. */
370     while ((option = getopt(argc, argv, "a:cfi:p:r")) != EOF)
371         switch (option) {
372         default:
373             die("usage error");
374             /* NOTREACHED */
375         case 'a':
376             config.root = optarg;
377             break;
378         case 'c':
379             config.flat = true;
380             config.concat = true;
381             break;
382         case 'f':
383             config.flat = true;
384             break;
385         case 'i':
386             config.index = fopen(optarg, "a");
387             if (config.index == NULL)
388                 sysdie("cannot open index %s for output", optarg);
389             break;
390         case 'p':
391             config.pattern = optarg;
392             break;
393         case 'r':
394             redirect = false;
395             break;
396         }
397 
398     /* Parse arguments, which should just be the batch file. */
399     argc -= optind;
400     argv += optind;
401     if (argc > 1)
402         die("usage error");
403     if (redirect) {
404         file = concatpath(innconf->pathlog, INN_PATH_ERRLOG);
405         if (freopen(file, "a", stderr) == NULL)
406             sysdie("cannot open %s for error output", file);
407     }
408     if (argc == 1)
409         if (freopen(argv[0], "r", stdin) == NULL)
410             sysdie("cannot open %s for input", argv[0]);
411 
412     /* Initialize the storage manager. */
413     if (!SMinit())
414         die("cannot initialize storage manager: %s", SMerrorstr);
415 
416     /* Read input. */
417     qp = QIOfdopen(fileno(stdin));
418     if (qp == NULL)
419         sysdie("cannot reopen input");
420     while ((line = QIOread(qp)) != NULL) {
421         if (*line == '\0' || *line == '#')
422             continue;
423 
424         /* Currently, we only handle tokens.  It would be good to handle
425            regular files as well, if for no other reason than for testing, but
426            we need a good way of faking an ARTHANDLE from a file. */
427         if (IsToken(line)) {
428             token = TextToToken(line);
429             art = SMretrieve(token, RETR_ALL);
430             if (art == NULL) {
431                 warn("cannot retrieve %s", line);
432                 continue;
433             }
434             process_article(art, line, &config);
435             SMfreearticle(art);
436         } else {
437             warn("%s is not a token", line);
438         }
439     }
440 
441     /* Close down the storage manager API. */
442     SMshutdown();
443 
444     /* If we read all our input, try to remove the file, and we're done. */
445     if (!QIOerror(qp)) {
446         fclose(stdin);
447         if (argv[0])
448             unlink(argv[0]);
449         exit(0);
450     }
451 
452     /* Otherwise, make an appropriate spool file. */
453     if (argv[0] == NULL)
454         file = concatpath(innconf->pathoutgoing, "archive");
455     else if (argv[0][0] == '/')
456         file = concat(argv[0], ".bch", (char *) 0);
457     else
458         file = concat(innconf->pathoutgoing, "/", argv[0], ".bch", (char *) 0);
459     spool = fopen(file, "a");
460     if (spool == NULL)
461         sysdie("cannot spool to %s", file);
462 
463     /* Write the rest of stdin to the spool file. */
464     status = 0;
465     if (fprintf(spool, "%s\n", line != NULL ? line : "") == EOF) {
466         syswarn("cannot start spool");
467         status = 1;
468     }
469     while (fgets(buffer, sizeof(buffer), stdin) != NULL)
470         if (fputs(buffer, spool) == EOF) {
471             syswarn("cannot write to spool");
472             status = 1;
473             break;
474         }
475     if (fclose(spool) == EOF) {
476         syswarn("cannot close spool");
477         status = 1;
478     }
479 
480     /* If we had a named input file, try to rename the spool. */
481     if (argv[0] != NULL && rename(file, argv[0]) < 0) {
482         syswarn("cannot rename spool");
483         status = 1;
484     }
485 
486     exit(status);
487 }
488