Write out new-style statcache data.
[cumulus.git] / scandir.cc
1 /* Recursively descend the filesystem and visit each file. */
2
3 #include <dirent.h>
4 #include <errno.h>
5 #include <fcntl.h>
6 #include <getopt.h>
7 #include <grp.h>
8 #include <pwd.h>
9 #include <stdint.h>
10 #include <stdio.h>
11 #include <stdlib.h>
12 #include <sys/stat.h>
13 #include <sys/sysmacros.h>
14 #include <sys/types.h>
15 #include <sys/wait.h>
16 #include <unistd.h>
17
18 #include <algorithm>
19 #include <fstream>
20 #include <iostream>
21 #include <list>
22 #include <set>
23 #include <sstream>
24 #include <string>
25 #include <vector>
26
27 #include "localdb.h"
28 #include "metadata.h"
29 #include "store.h"
30 #include "sha1.h"
31 #include "statcache.h"
32 #include "util.h"
33
34 using std::list;
35 using std::string;
36 using std::vector;
37 using std::ostream;
38
39 /* Version information.  This will be filled in by the Makefile. */
40 #ifndef LBS_VERSION
41 #define LBS_VERSION Unknown
42 #endif
43 #define LBS_STRINGIFY(s) LBS_STRINGIFY2(s)
44 #define LBS_STRINGIFY2(s) #s
45 static const char lbs_version[] = LBS_STRINGIFY(LBS_VERSION);
46
47 static TarSegmentStore *tss = NULL;
48 static MetadataWriter *metawriter = NULL;
49
50 /* Buffer for holding a single block of data read from a file. */
51 static const size_t LBS_BLOCK_SIZE = 1024 * 1024;
52 static char *block_buf;
53
54 /* Local database, which tracks objects written in this and previous
55  * invocations to help in creating incremental snapshots. */
56 LocalDb *db;
57
58 /* Stat cache, which stored data locally to speed the backup process by quickly
59  * skipping files which have not changed. */
60 StatCache *statcache;
61
62 /* Keep track of all segments which are needed to reconstruct the snapshot. */
63 std::set<string> segment_list;
64
65 /* Selection of files to include/exclude in the snapshot. */
66 std::list<string> includes;         // Paths in which files should be saved
67 std::list<string> excludes;         // Paths which will not be saved
68 std::list<string> searches;         // Directories we don't want to save, but
69                                     //   do want to descend searching for data
70                                     //   in included paths
71
72 bool relative_paths = true;
73
74 /* Ensure that the given segment is listed as a dependency of the current
75  * snapshot. */
76 void add_segment(const string& segment)
77 {
78     segment_list.insert(segment);
79 }
80
81 /* Read data from a file descriptor and return the amount of data read.  A
82  * short read (less than the requested size) will only occur if end-of-file is
83  * hit. */
84 ssize_t file_read(int fd, char *buf, size_t maxlen)
85 {
86     size_t bytes_read = 0;
87
88     while (true) {
89         ssize_t res = read(fd, buf, maxlen);
90         if (res < 0) {
91             if (errno == EINTR)
92                 continue;
93             fprintf(stderr, "error reading file: %m\n");
94             return -1;
95         } else if (res == 0) {
96             break;
97         } else {
98             bytes_read += res;
99             buf += res;
100             maxlen -= res;
101         }
102     }
103
104     return bytes_read;
105 }
106
107 /* Read the contents of a file (specified by an open file descriptor) and copy
108  * the data to the store.  Returns the size of the file (number of bytes
109  * dumped), or -1 on error. */
110 int64_t dumpfile(int fd, dictionary &file_info, const string &path,
111                  struct stat& stat_buf)
112 {
113     int64_t size = 0;
114     list<string> object_list;
115     const char *status = NULL;          /* Status indicator printed out */
116
117     /* Look up this file in the old stat cache, if we can.  If the stat
118      * information indicates that the file has not changed, do not bother
119      * re-reading the entire contents. */
120     bool cached = false;
121
122     if (statcache->Find(path, &stat_buf)) {
123         cached = true;
124         const list<ObjectReference> &blocks = statcache->get_blocks();
125
126         /* If any of the blocks in the object have been expired, then we should
127          * fall back to fully reading in the file. */
128         for (list<ObjectReference>::const_iterator i = blocks.begin();
129              i != blocks.end(); ++i) {
130             const ObjectReference &ref = *i;
131             if (!db->IsAvailable(ref)) {
132                 cached = false;
133                 status = "repack";
134                 break;
135             }
136         }
137
138         /* If everything looks okay, use the cached information */
139         if (cached) {
140             file_info["checksum"] = statcache->get_checksum();
141             for (list<ObjectReference>::const_iterator i = blocks.begin();
142                  i != blocks.end(); ++i) {
143                 const ObjectReference &ref = *i;
144                 object_list.push_back(ref.to_string());
145                 segment_list.insert(ref.get_segment());
146                 db->UseObject(ref);
147             }
148             size = stat_buf.st_size;
149         }
150     }
151
152     /* If the file is new or changed, we must read in the contents a block at a
153      * time. */
154     if (!cached) {
155         SHA1Checksum hash;
156         while (true) {
157             ssize_t bytes = file_read(fd, block_buf, LBS_BLOCK_SIZE);
158             if (bytes == 0)
159                 break;
160             if (bytes < 0) {
161                 fprintf(stderr, "Backup contents for %s may be incorrect\n",
162                         path.c_str());
163                 break;
164             }
165
166             hash.process(block_buf, bytes);
167
168             // Either find a copy of this block in an already-existing segment,
169             // or index it so it can be re-used in the future
170             double block_age = 0.0;
171             SHA1Checksum block_hash;
172             block_hash.process(block_buf, bytes);
173             string block_csum = block_hash.checksum_str();
174             ObjectReference ref = db->FindObject(block_csum, bytes);
175
176             // Store a copy of the object if one does not yet exist
177             if (ref.get_segment().size() == 0) {
178                 LbsObject *o = new LbsObject;
179                 int object_group;
180
181                 /* We might still have seen this checksum before, if the object
182                  * was stored at some time in the past, but we have decided to
183                  * clean the segment the object was originally stored in
184                  * (FindObject will not return such objects).  When rewriting
185                  * the object contents, put it in a separate group, so that old
186                  * objects get grouped together.  The hope is that these old
187                  * objects will continue to be used in the future, and we
188                  * obtain segments which will continue to be well-utilized.
189                  * Additionally, keep track of the age of the data by looking
190                  * up the age of the block which was expired and using that
191                  * instead of the current time. */
192                 if (db->IsOldObject(block_csum, bytes,
193                                     &block_age, &object_group)) {
194                     if (object_group == 0) {
195                         o->set_group("data");
196                     } else {
197                         char group[32];
198                         sprintf(group, "compacted-%d", object_group);
199                         o->set_group(group);
200                     }
201                     if (status == NULL)
202                         status = "partial";
203                 } else {
204                     o->set_group("data");
205                     status = "new";
206                 }
207
208                 o->set_data(block_buf, bytes);
209                 o->write(tss);
210                 ref = o->get_ref();
211                 db->StoreObject(ref, block_csum, bytes, block_age);
212                 delete o;
213             }
214
215             object_list.push_back(ref.to_string());
216             segment_list.insert(ref.get_segment());
217             db->UseObject(ref);
218             size += bytes;
219
220             if (status == NULL)
221                 status = "old";
222         }
223
224         file_info["checksum"] = hash.checksum_str();
225     }
226
227     if (status != NULL)
228         printf("    [%s]\n", status);
229
230     statcache->Save(path, &stat_buf, file_info["checksum"], object_list);
231
232     string blocklist = "";
233     for (list<string>::iterator i = object_list.begin();
234          i != object_list.end(); ++i) {
235         if (i != object_list.begin())
236             blocklist += "\n    ";
237         blocklist += *i;
238     }
239     file_info["data"] = blocklist;
240
241     return size;
242 }
243
244 /* Dump a specified filesystem object (file, directory, etc.) based on its
245  * inode information.  If the object is a regular file, an open filehandle is
246  * provided. */
247 void dump_inode(const string& path,         // Path within snapshot
248                 const string& fullpath,     // Path to object in filesystem
249                 struct stat& stat_buf,      // Results of stat() call
250                 int fd)                     // Open filehandle if regular file
251 {
252     char *buf;
253     dictionary file_info;
254     int64_t file_size;
255     ssize_t len;
256
257     printf("%s\n", path.c_str());
258
259     file_info["mode"] = encode_int(stat_buf.st_mode & 07777, 8);
260     file_info["ctime"] = encode_int(stat_buf.st_ctime);
261     file_info["mtime"] = encode_int(stat_buf.st_mtime);
262     file_info["user"] = encode_int(stat_buf.st_uid);
263     file_info["group"] = encode_int(stat_buf.st_gid);
264
265     struct passwd *pwd = getpwuid(stat_buf.st_uid);
266     if (pwd != NULL) {
267         file_info["user"] += " (" + uri_encode(pwd->pw_name) + ")";
268     }
269
270     struct group *grp = getgrgid(stat_buf.st_gid);
271     if (pwd != NULL) {
272         file_info["group"] += " (" + uri_encode(grp->gr_name) + ")";
273     }
274
275     if (stat_buf.st_nlink > 1 && (stat_buf.st_mode & S_IFMT) != S_IFDIR) {
276         file_info["links"] = encode_int(stat_buf.st_nlink);
277     }
278
279     file_info["inode"] = encode_int(major(stat_buf.st_dev))
280         + "/" + encode_int(minor(stat_buf.st_dev))
281         + "/" + encode_int(stat_buf.st_ino);
282
283     char inode_type;
284
285     switch (stat_buf.st_mode & S_IFMT) {
286     case S_IFIFO:
287         inode_type = 'p';
288         break;
289     case S_IFSOCK:
290         inode_type = 's';
291         break;
292     case S_IFBLK:
293     case S_IFCHR:
294         inode_type = ((stat_buf.st_mode & S_IFMT) == S_IFBLK) ? 'b' : 'c';
295         file_info["device"] = encode_int(major(stat_buf.st_rdev))
296             + "/" + encode_int(minor(stat_buf.st_rdev));
297         break;
298     case S_IFLNK:
299         inode_type = 'l';
300
301         /* Use the reported file size to allocate a buffer large enough to read
302          * the symlink.  Allocate slightly more space, so that we ask for more
303          * bytes than we expect and so check for truncation. */
304         buf = new char[stat_buf.st_size + 2];
305         len = readlink(fullpath.c_str(), buf, stat_buf.st_size + 1);
306         if (len < 0) {
307             fprintf(stderr, "error reading symlink: %m\n");
308         } else if (len <= stat_buf.st_size) {
309             buf[len] = '\0';
310             file_info["target"] = uri_encode(buf);
311         } else if (len > stat_buf.st_size) {
312             fprintf(stderr, "error reading symlink: name truncated\n");
313         }
314
315         delete[] buf;
316         break;
317     case S_IFREG:
318         inode_type = 'f';
319
320         file_size = dumpfile(fd, file_info, path, stat_buf);
321         file_info["size"] = encode_int(file_size);
322
323         if (file_size < 0)
324             return;             // error occurred; do not dump file
325
326         if (file_size != stat_buf.st_size) {
327             fprintf(stderr, "Warning: Size of %s changed during reading\n",
328                     path.c_str());
329         }
330
331         break;
332     case S_IFDIR:
333         inode_type = 'd';
334         break;
335
336     default:
337         fprintf(stderr, "Unknown inode type: mode=%x\n", stat_buf.st_mode);
338         return;
339     }
340
341     file_info["type"] = string(1, inode_type);
342
343     metawriter->add(path, file_info);
344 }
345
346 void scanfile(const string& path, bool include)
347 {
348     int fd = -1;
349     long flags;
350     struct stat stat_buf;
351     list<string> refs;
352
353     string true_path;
354     if (relative_paths)
355         true_path = path;
356     else
357         true_path = "/" + path;
358
359     // Set to true if we should scan through the contents of this directory,
360     // but not actually back files up
361     bool scan_only = false;
362
363     // Check this file against the include/exclude list to see if it should be
364     // considered
365     for (list<string>::iterator i = includes.begin();
366          i != includes.end(); ++i) {
367         if (path == *i) {
368             printf("Including %s\n", path.c_str());
369             include = true;
370         }
371     }
372
373     for (list<string>::iterator i = excludes.begin();
374          i != excludes.end(); ++i) {
375         if (path == *i) {
376             printf("Excluding %s\n", path.c_str());
377             include = false;
378         }
379     }
380
381     for (list<string>::iterator i = searches.begin();
382          i != searches.end(); ++i) {
383         if (path == *i) {
384             printf("Scanning %s\n", path.c_str());
385             scan_only = true;
386         }
387     }
388
389     if (!include && !scan_only)
390         return;
391
392     if (lstat(true_path.c_str(), &stat_buf) < 0) {
393         fprintf(stderr, "lstat(%s): %m\n", path.c_str());
394         return;
395     }
396
397     if ((stat_buf.st_mode & S_IFMT) == S_IFREG) {
398         /* Be paranoid when opening the file.  We have no guarantee that the
399          * file was not replaced between the stat() call above and the open()
400          * call below, so we might not even be opening a regular file.  We
401          * supply flags to open to to guard against various conditions before
402          * we can perform an lstat to check that the file is still a regular
403          * file:
404          *   - O_NOFOLLOW: in the event the file was replaced by a symlink
405          *   - O_NONBLOCK: prevents open() from blocking if the file was
406          *     replaced by a fifo
407          * We also add in O_NOATIME, since this may reduce disk writes (for
408          * inode updates).  However, O_NOATIME may result in EPERM, so if the
409          * initial open fails, try again without O_NOATIME.  */
410         fd = open(true_path.c_str(), O_RDONLY|O_NOATIME|O_NOFOLLOW|O_NONBLOCK);
411         if (fd < 0) {
412             fd = open(true_path.c_str(), O_RDONLY|O_NOFOLLOW|O_NONBLOCK);
413         }
414         if (fd < 0) {
415             fprintf(stderr, "Unable to open file %s: %m\n", path.c_str());
416             return;
417         }
418
419         /* Drop the use of the O_NONBLOCK flag; we only wanted that for file
420          * open. */
421         flags = fcntl(fd, F_GETFL);
422         fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
423
424         /* Perform the stat call again, and check that we still have a regular
425          * file. */
426         if (fstat(fd, &stat_buf) < 0) {
427             fprintf(stderr, "fstat: %m\n");
428             close(fd);
429             return;
430         }
431
432         if ((stat_buf.st_mode & S_IFMT) != S_IFREG) {
433             fprintf(stderr, "file is no longer a regular file!\n");
434             close(fd);
435             return;
436         }
437     }
438
439     dump_inode(path, true_path, stat_buf, fd);
440
441     if (fd >= 0)
442         close(fd);
443
444     // If we hit a directory, now that we've written the directory itself,
445     // recursively scan the directory.
446     if ((stat_buf.st_mode & S_IFMT) == S_IFDIR) {
447         DIR *dir = opendir(true_path.c_str());
448
449         if (dir == NULL) {
450             fprintf(stderr, "Error: %m\n");
451             return;
452         }
453
454         struct dirent *ent;
455         vector<string> contents;
456         while ((ent = readdir(dir)) != NULL) {
457             string filename(ent->d_name);
458             if (filename == "." || filename == "..")
459                 continue;
460             contents.push_back(filename);
461         }
462
463         closedir(dir);
464
465         sort(contents.begin(), contents.end());
466
467         for (vector<string>::iterator i = contents.begin();
468              i != contents.end(); ++i) {
469             const string& filename = *i;
470             if (path == ".")
471                 scanfile(filename, include);
472             else
473                 scanfile(path + "/" + filename, include);
474         }
475     }
476 }
477
478 /* Include the specified file path in the backups.  Append the path to the
479  * includes list, and to ensure that we actually see the path when scanning the
480  * directory tree, add all the parent directories to the search list, which
481  * means we will scan through the directory listing even if the files
482  * themselves are excluded from being backed up. */
483 void add_include(const char *path)
484 {
485     printf("Add: %s\n", path);
486     /* Was an absolute path specified?  If so, we'll need to start scanning
487      * from the root directory.  Make sure that the user was consistent in
488      * providing either all relative paths or all absolute paths. */
489     if (path[0] == '/') {
490         if (includes.size() > 0 && relative_paths == true) {
491             fprintf(stderr,
492                     "Error: Cannot mix relative and absolute paths!\n");
493             exit(1);
494         }
495
496         relative_paths = false;
497
498         // Skip over leading '/'
499         path++;
500     } else if (relative_paths == false && path[0] != '/') {
501         fprintf(stderr, "Error: Cannot mix relative and absolute paths!\n");
502         exit(1);
503     }
504
505     includes.push_back(path);
506
507     /* Split the specified path into directory components, and ensure that we
508      * descend into all the directories along the path. */
509     const char *slash = path;
510
511     if (path[0] == '\0')
512         return;
513
514     while ((slash = strchr(slash + 1, '/')) != NULL) {
515         string component(path, slash - path);
516         searches.push_back(component);
517     }
518 }
519
520 void usage(const char *program)
521 {
522     fprintf(
523         stderr,
524         "LBS %s\n\n"
525         "Usage: %s [OPTION]... --dest=DEST PATHS...\n"
526         "Produce backup snapshot of files in SOURCE and store to DEST.\n"
527         "\n"
528         "Options:\n"
529         "  --dest=PATH          path where backup is to be written [REQUIRED]\n"
530         "  --exclude=PATH       exclude files in PATH from snapshot\n"
531         "  --localdb=PATH       local backup metadata is stored in PATH\n"
532         "  --filter=COMMAND     program through which to filter segment data\n"
533         "                           (defaults to \"bzip2 -c\")\n"
534         "  --filter-extension=EXT\n"
535         "                       string to append to segment files\n"
536         "                           (defaults to \".bz2\")\n"
537         "  --signature-filter=COMMAND\n"
538         "                       program though which to filter descriptor\n"
539         "  --scheme=NAME        optional name for this snapshot\n",
540         lbs_version, program
541     );
542 }
543
544 int main(int argc, char *argv[])
545 {
546     string backup_dest = "";
547     string localdb_dir = "";
548     string backup_scheme = "";
549     string signature_filter = "";
550
551     while (1) {
552         static struct option long_options[] = {
553             {"localdb", 1, 0, 0},           // 0
554             {"exclude", 1, 0, 0},           // 1
555             {"filter", 1, 0, 0},            // 2
556             {"filter-extension", 1, 0, 0},  // 3
557             {"dest", 1, 0, 0},              // 4
558             {"scheme", 1, 0, 0},            // 5
559             {"signature-filter", 1, 0, 0},  // 6
560             {NULL, 0, 0, 0},
561         };
562
563         int long_index;
564         int c = getopt_long(argc, argv, "", long_options, &long_index);
565
566         if (c == -1)
567             break;
568
569         if (c == 0) {
570             switch (long_index) {
571             case 0:     // --localdb
572                 localdb_dir = optarg;
573                 break;
574             case 1:     // --exclude
575                 if (optarg[0] != '/')
576                     excludes.push_back(optarg);
577                 else
578                     excludes.push_back(optarg + 1);
579                 break;
580             case 2:     // --filter
581                 filter_program = optarg;
582                 break;
583             case 3:     // --filter-extension
584                 filter_extension = optarg;
585                 break;
586             case 4:     // --dest
587                 backup_dest = optarg;
588                 break;
589             case 5:     // --scheme
590                 backup_scheme = optarg;
591                 break;
592             case 6:     // --signature-filter
593                 signature_filter = optarg;
594                 break;
595             default:
596                 fprintf(stderr, "Unhandled long option!\n");
597                 return 1;
598             }
599         } else {
600             usage(argv[0]);
601             return 1;
602         }
603     }
604
605     if (optind == argc) {
606         usage(argv[0]);
607         return 1;
608     }
609
610     searches.push_back(".");
611     for (int i = optind; i < argc; i++)
612         add_include(argv[i]);
613
614     if (backup_dest == "") {
615         fprintf(stderr,
616                 "Error: Backup destination must be specified with --dest=\n");
617         usage(argv[0]);
618         return 1;
619     }
620
621     // Default for --localdb is the same as --dest
622     if (localdb_dir == "") {
623         localdb_dir = backup_dest;
624     }
625
626     // Dump paths for debugging/informational purposes
627     {
628         list<string>::const_iterator i;
629
630         printf("LBS Version: %s\n", lbs_version);
631
632         printf("--dest=%s\n--localdb=%s\n\n",
633                backup_dest.c_str(), localdb_dir.c_str());
634
635         printf("Includes:\n");
636         for (i = includes.begin(); i != includes.end(); ++i)
637             printf("    %s\n", i->c_str());
638
639         printf("Excludes:\n");
640         for (i = excludes.begin(); i != excludes.end(); ++i)
641             printf("    %s\n", i->c_str());
642
643         printf("Searching:\n");
644         for (i = searches.begin(); i != searches.end(); ++i)
645             printf("    %s\n", i->c_str());
646     }
647
648     block_buf = new char[LBS_BLOCK_SIZE];
649
650     /* Store the time when the backup started, so it can be included in the
651      * snapshot name. */
652     time_t now;
653     struct tm time_buf;
654     char desc_buf[256];
655     time(&now);
656     localtime_r(&now, &time_buf);
657     strftime(desc_buf, sizeof(desc_buf), "%Y%m%dT%H%M%S", &time_buf);
658
659     /* Open the local database which tracks all objects that are stored
660      * remotely, for efficient incrementals.  Provide it with the name of this
661      * snapshot. */
662     string database_path = localdb_dir + "/localdb.sqlite";
663     db = new LocalDb;
664     db->Open(database_path.c_str(), desc_buf,
665              backup_scheme.size() ? backup_scheme.c_str() : NULL);
666
667     tss = new TarSegmentStore(backup_dest, db);
668
669     /* Initialize the stat cache, for skipping over unchanged files. */
670     statcache = new StatCache;
671     statcache->Open(localdb_dir.c_str(), desc_buf,
672                     backup_scheme.size() ? backup_scheme.c_str() : NULL);
673
674     metawriter = new MetadataWriter(tss, localdb_dir.c_str(), desc_buf,
675                                     backup_scheme.size()
676                                         ? backup_scheme.c_str()
677                                         : NULL);
678
679     scanfile(".", false);
680
681     ObjectReference root_ref = metawriter->close();
682     add_segment(root_ref.get_segment());
683     string backup_root = root_ref.to_string();
684
685     statcache->Close();
686     delete statcache;
687
688     delete metawriter;
689
690     tss->sync();
691     tss->dump_stats();
692     delete tss;
693
694     /* Write out a checksums file which lists the checksums for all the
695      * segments included in this snapshot.  The format is designed so that it
696      * may be easily verified using the sha1sums command. */
697     const char csum_type[] = "sha1";
698     string checksum_filename = backup_dest + "/snapshot-";
699     if (backup_scheme.size() > 0)
700         checksum_filename += backup_scheme + "-";
701     checksum_filename = checksum_filename + desc_buf + "." + csum_type + "sums";
702     FILE *checksums = fopen(checksum_filename.c_str(), "w");
703     if (checksums != NULL) {
704         for (std::set<string>::iterator i = segment_list.begin();
705              i != segment_list.end(); ++i) {
706             string seg_path, seg_csum;
707             if (db->GetSegmentChecksum(*i, &seg_path, &seg_csum)) {
708                 const char *raw_checksum = NULL;
709                 if (strncmp(seg_csum.c_str(), csum_type,
710                             strlen(csum_type)) == 0) {
711                     raw_checksum = seg_csum.c_str() + strlen(csum_type);
712                     if (*raw_checksum == '=')
713                         raw_checksum++;
714                     else
715                         raw_checksum = NULL;
716                 }
717
718                 if (raw_checksum != NULL)
719                     fprintf(checksums, "%s *%s\n",
720                             raw_checksum, seg_path.c_str());
721             }
722         }
723         fclose(checksums);
724     } else {
725         fprintf(stderr, "ERROR: Unable to write checksums file: %m\n");
726     }
727
728     db->Close();
729
730     /* Write a backup descriptor file, which says which segments are needed and
731      * where to start to restore this snapshot.  The filename is based on the
732      * current time.  If a signature filter program was specified, filter the
733      * data through that to give a chance to sign the descriptor contents. */
734     string desc_filename = backup_dest + "/snapshot-";
735     if (backup_scheme.size() > 0)
736         desc_filename += backup_scheme + "-";
737     desc_filename = desc_filename + desc_buf + ".lbs";
738
739     int descriptor_fd = open(desc_filename.c_str(), O_WRONLY | O_CREAT, 0666);
740     if (descriptor_fd < 0) {
741         fprintf(stderr, "Unable to open descriptor output file: %m\n");
742         return 1;
743     }
744     pid_t signature_pid = 0;
745     if (signature_filter.size() > 0) {
746         int new_fd = spawn_filter(descriptor_fd, signature_filter.c_str(),
747                                   &signature_pid);
748         close(descriptor_fd);
749         descriptor_fd = new_fd;
750     }
751     FILE *descriptor = fdopen(descriptor_fd, "w");
752
753     fprintf(descriptor, "Format: LBS Snapshot v0.6\n");
754     fprintf(descriptor, "Producer: LBS %s\n", lbs_version);
755     strftime(desc_buf, sizeof(desc_buf), "%Y-%m-%d %H:%M:%S %z", &time_buf);
756     fprintf(descriptor, "Date: %s\n", desc_buf);
757     if (backup_scheme.size() > 0)
758         fprintf(descriptor, "Scheme: %s\n", backup_scheme.c_str());
759     fprintf(descriptor, "Root: %s\n", backup_root.c_str());
760
761     SHA1Checksum checksum_csum;
762     if (checksum_csum.process_file(checksum_filename.c_str())) {
763         string csum = checksum_csum.checksum_str();
764         fprintf(descriptor, "Checksums: %s\n", csum.c_str());
765     }
766
767     fprintf(descriptor, "Segments:\n");
768     for (std::set<string>::iterator i = segment_list.begin();
769          i != segment_list.end(); ++i) {
770         fprintf(descriptor, "    %s\n", i->c_str());
771     }
772
773     fclose(descriptor);
774
775     if (signature_pid) {
776         int status;
777         waitpid(signature_pid, &status, 0);
778
779         if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
780             throw IOException("Signature filter process error");
781         }
782     }
783
784     return 0;
785 }