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