Improve tracking of segments and segment utilization.
[cumulus.git] / main.cc
diff --git a/main.cc b/main.cc
index c700173..e10a04a 100644 (file)
--- a/main.cc
+++ b/main.cc
@@ -1,7 +1,6 @@
-/* Cumulus: Smart Filesystem Backup to Dumb Servers
- *
- * Copyright (C) 2006-2008  The Regents of the University of California
- * Written by Michael Vrable <mvrable@cs.ucsd.edu>
+/* Cumulus: Efficient Filesystem Backup to the Cloud
+ * Copyright (C) 2006-2009, 2012 The Cumulus Developers
+ * See the AUTHORS file for a list of contributors.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 #include <string>
 #include <vector>
 
+#include "exclude.h"
+#include "hash.h"
 #include "localdb.h"
 #include "metadata.h"
 #include "remote.h"
 #include "store.h"
-#include "sha1.h"
 #include "subfile.h"
 #include "util.h"
+#include "third_party/sha1.h"
 
 using std::list;
 using std::map;
@@ -81,34 +82,72 @@ static char *block_buf;
  * invocations to help in creating incremental snapshots. */
 LocalDb *db;
 
-/* Keep track of all segments which are needed to reconstruct the snapshot. */
-std::set<string> segment_list;
-
 /* Snapshot intent: 1=daily, 7=weekly, etc.  This is not used directly, but is
  * stored in the local database and can help guide segment cleaning and
  * snapshot expiration policies. */
 double snapshot_intent = 1.0;
 
 /* Selection of files to include/exclude in the snapshot. */
-std::list<string> includes;         // Paths in which files should be saved
-std::list<string> excludes;         // Paths which will not be saved
-std::list<string> excluded_names;   // Directories which will not be saved
-std::list<string> searches;         // Directories we don't want to save, but
-                                    //   do want to descend searching for data
-                                    //   in included paths
-
-bool relative_paths = true;
+PathFilterList filter_rules;
 
 bool flag_rebuild_statcache = false;
 
 /* Whether verbose output is enabled. */
 bool verbose = false;
 
-/* Ensure that the given segment is listed as a dependency of the current
- * snapshot. */
-void add_segment(const string& segment)
+/* Attempts to open a regular file read-only, but with safety checks for files
+ * that might not be fully trusted. */
+int safe_open(const string& path, struct stat *stat_buf)
 {
-    segment_list.insert(segment);
+    int fd;
+
+    /* Be paranoid when opening the file.  We have no guarantee that the
+     * file was not replaced between the stat() call above and the open()
+     * call below, so we might not even be opening a regular file.  We
+     * supply flags to open to to guard against various conditions before
+     * we can perform an lstat to check that the file is still a regular
+     * file:
+     *   - O_NOFOLLOW: in the event the file was replaced by a symlink
+     *   - O_NONBLOCK: prevents open() from blocking if the file was
+     *     replaced by a fifo
+     * We also add in O_NOATIME, since this may reduce disk writes (for
+     * inode updates).  However, O_NOATIME may result in EPERM, so if the
+     * initial open fails, try again without O_NOATIME.  */
+    fd = open(path.c_str(), O_RDONLY|O_NOATIME|O_NOFOLLOW|O_NONBLOCK);
+    if (fd < 0) {
+        fd = open(path.c_str(), O_RDONLY|O_NOFOLLOW|O_NONBLOCK);
+    }
+    if (fd < 0) {
+        fprintf(stderr, "Unable to open file %s: %m\n", path.c_str());
+        return -1;
+    }
+
+    /* Drop the use of the O_NONBLOCK flag; we only wanted that for file
+     * open. */
+    long flags = fcntl(fd, F_GETFL);
+    fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
+
+    /* Re-check file attributes, storing them into stat_buf if that is
+     * non-NULL. */
+    struct stat internal_stat_buf;
+    if (stat_buf == NULL)
+        stat_buf = &internal_stat_buf;
+
+    /* Perform the stat call again, and check that we still have a regular
+     * file. */
+    if (fstat(fd, stat_buf) < 0) {
+        fprintf(stderr, "fstat: %m\n");
+        close(fd);
+        return -1;
+    }
+
+    if ((stat_buf->st_mode & S_IFMT) != S_IFREG) {
+        fprintf(stderr, "file is no longer a regular file!\n");
+        close(fd);
+        return -1;
+    }
+
+    return fd;
 }
 
 /* Read data from a file descriptor and return the amount of data read.  A
@@ -183,8 +222,6 @@ int64_t dumpfile(int fd, dictionary &file_info, const string &path,
                  i != old_blocks.end(); ++i) {
                 const ObjectReference &ref = *i;
                 object_list.push_back(ref.to_string());
-                if (ref.is_normal())
-                    add_segment(ref.get_segment());
                 db->UseObject(ref);
             }
             size = stat_buf.st_size;
@@ -194,7 +231,7 @@ int64_t dumpfile(int fd, dictionary &file_info, const string &path,
     /* If the file is new or changed, we must read in the contents a block at a
      * time. */
     if (!cached) {
-        SHA1Checksum hash;
+        Hash *hash = Hash::New();
         Subfile subfile(db);
         subfile.load_old_blocks(old_blocks);
 
@@ -208,7 +245,7 @@ int64_t dumpfile(int fd, dictionary &file_info, const string &path,
                 break;
             }
 
-            hash.process(block_buf, bytes);
+            hash->update(block_buf, bytes);
 
             // Sparse file processing: if we read a block of all zeroes, encode
             // that explicitly.
@@ -225,9 +262,10 @@ int64_t dumpfile(int fd, dictionary &file_info, const string &path,
             double block_age = 0.0;
             ObjectReference ref;
 
-            SHA1Checksum block_hash;
-            block_hash.process(block_buf, bytes);
-            string block_csum = block_hash.checksum_str();
+            Hash *hash = Hash::New();
+            hash->update(block_buf, bytes);
+            string block_csum = hash->digest_str();
+            delete hash;
 
             if (all_zero) {
                 ref = ObjectReference(ObjectReference::REF_ZERO);
@@ -283,8 +321,6 @@ int64_t dumpfile(int fd, dictionary &file_info, const string &path,
             while (!refs.empty()) {
                 ref = refs.front(); refs.pop_front();
                 object_list.push_back(ref.to_string());
-                if (ref.is_normal())
-                    add_segment(ref.get_segment());
                 db->UseObject(ref);
             }
             size += bytes;
@@ -293,7 +329,8 @@ int64_t dumpfile(int fd, dictionary &file_info, const string &path,
                 status = "old";
         }
 
-        file_info["checksum"] = hash.checksum_str();
+        file_info["checksum"] = hash->digest_str();
+        delete hash;
     }
 
     // Sanity check: if we are rebuilding the statcache, but the file looks
@@ -463,123 +500,81 @@ void dump_inode(const string& path,         // Path within snapshot
     metawriter->add(file_info);
 }
 
-void scanfile(const string& path, bool include)
+/* Converts a path to the normalized form used in the metadata log.  Paths are
+ * written as relative (without any leading slashes).  The root directory is
+ * referred to as ".". */
+string metafile_path(const string& path)
 {
-    int fd = -1;
-    long flags;
-    struct stat stat_buf;
-    list<string> refs;
-
-    string true_path;
-    if (relative_paths)
-        true_path = path;
-    else
-        true_path = "/" + path;
-
-    // Set to true if we should scan through the contents of this directory,
-    // but not actually back files up
-    bool scan_only = false;
-
-    // Check this file against the include/exclude list to see if it should be
-    // considered
-    for (list<string>::iterator i = includes.begin();
-         i != includes.end(); ++i) {
-        if (path == *i) {
-            include = true;
-        }
-    }
-
-    for (list<string>::iterator i = excludes.begin();
-         i != excludes.end(); ++i) {
-        if (path == *i) {
-            include = false;
-        }
-    }
+    const char *newpath = path.c_str();
+    if (*newpath == '/')
+        newpath++;
+    if (*newpath == '\0')
+        newpath = ".";
+    return newpath;
+}
 
-    if (excluded_names.size() > 0) {
-        std::string name = path;
-        std::string::size_type last_slash = name.rfind('/');
-        if (last_slash != std::string::npos) {
-            name.replace(0, last_slash + 1, "");
-        }
+void try_merge_filter(const string& path, const string& basedir)
+{
+    struct stat stat_buf;
+    if (lstat(path.c_str(), &stat_buf) < 0)
+        return;
+    if ((stat_buf.st_mode & S_IFMT) != S_IFREG)
+        return;
+    int fd = safe_open(path, NULL);
+    if (fd < 0)
+        return;
 
-        for (list<string>::iterator i = excluded_names.begin();
-             i != excluded_names.end(); ++i) {
-            if (name == *i) {
-                include = false;
-            }
-        }
+    /* As a very crude limit on the complexity of merge rules, only read up to
+     * one block (1 MB) worth of data.  If the file doesn't seems like it might
+     * be larger than that, don't parse the rules in it. */
+    ssize_t bytes = file_read(fd, block_buf, LBS_BLOCK_SIZE);
+    close(fd);
+    if (bytes < 0 || bytes >= static_cast<ssize_t>(LBS_BLOCK_SIZE - 1)) {
+        /* TODO: Add more strict resource limits on merge files? */
+        fprintf(stderr,
+                "Unable to read filter merge file (possibly size too large\n");
+        return;
     }
+    filter_rules.merge_patterns(metafile_path(path), basedir,
+                                string(block_buf, bytes));
+}
 
-    for (list<string>::iterator i = searches.begin();
-         i != searches.end(); ++i) {
-        if (path == *i) {
-            scan_only = true;
-        }
-    }
+void scanfile(const string& path)
+{
+    int fd = -1;
+    struct stat stat_buf;
+    list<string> refs;
 
-    if (!include && !scan_only)
-        return;
+    string output_path = metafile_path(path);
 
-    if (lstat(true_path.c_str(), &stat_buf) < 0) {
+    if (lstat(path.c_str(), &stat_buf) < 0) {
         fprintf(stderr, "lstat(%s): %m\n", path.c_str());
         return;
     }
 
-    if ((stat_buf.st_mode & S_IFMT) == S_IFREG) {
-        /* Be paranoid when opening the file.  We have no guarantee that the
-         * file was not replaced between the stat() call above and the open()
-         * call below, so we might not even be opening a regular file.  We
-         * supply flags to open to to guard against various conditions before
-         * we can perform an lstat to check that the file is still a regular
-         * file:
-         *   - O_NOFOLLOW: in the event the file was replaced by a symlink
-         *   - O_NONBLOCK: prevents open() from blocking if the file was
-         *     replaced by a fifo
-         * We also add in O_NOATIME, since this may reduce disk writes (for
-         * inode updates).  However, O_NOATIME may result in EPERM, so if the
-         * initial open fails, try again without O_NOATIME.  */
-        fd = open(true_path.c_str(), O_RDONLY|O_NOATIME|O_NOFOLLOW|O_NONBLOCK);
-        if (fd < 0) {
-            fd = open(true_path.c_str(), O_RDONLY|O_NOFOLLOW|O_NONBLOCK);
-        }
-        if (fd < 0) {
-            fprintf(stderr, "Unable to open file %s: %m\n", path.c_str());
-            return;
-        }
-
-        /* Drop the use of the O_NONBLOCK flag; we only wanted that for file
-         * open. */
-        flags = fcntl(fd, F_GETFL);
-        fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
-
-        /* Perform the stat call again, and check that we still have a regular
-         * file. */
-        if (fstat(fd, &stat_buf) < 0) {
-            fprintf(stderr, "fstat: %m\n");
-            close(fd);
-            return;
-        }
+    bool is_directory = ((stat_buf.st_mode & S_IFMT) == S_IFDIR);
+    if (!filter_rules.is_included(output_path, is_directory))
+        return;
 
-        if ((stat_buf.st_mode & S_IFMT) != S_IFREG) {
-            fprintf(stderr, "file is no longer a regular file!\n");
-            close(fd);
+    if ((stat_buf.st_mode & S_IFMT) == S_IFREG) {
+        fd = safe_open(path, &stat_buf);
+        if (fd < 0)
             return;
-        }
     }
 
-    dump_inode(path, true_path, stat_buf, fd);
+    dump_inode(output_path, path, stat_buf, fd);
 
     if (fd >= 0)
         close(fd);
 
-    // If we hit a directory, now that we've written the directory itself,
-    // recursively scan the directory.
-    if ((stat_buf.st_mode & S_IFMT) == S_IFDIR) {
-        DIR *dir = opendir(true_path.c_str());
+    /* If we hit a directory, now that we've written the directory itself,
+     * recursively scan the directory. */
+    if (is_directory) {
+        DIR *dir = opendir(path.c_str());
 
         if (dir == NULL) {
-            fprintf(stderr, "Error: %m\n");
+            fprintf(stderr, "Error reading directory %s: %m\n",
+                    path.c_str());
             return;
         }
 
@@ -596,55 +591,42 @@ void scanfile(const string& path, bool include)
 
         sort(contents.begin(), contents.end());
 
+        filter_rules.save();
+
+        /* First pass through the directory items: look for any filter rules to
+         * merge and do so. */
         for (vector<string>::iterator i = contents.begin();
              i != contents.end(); ++i) {
-            const string& filename = *i;
+            string filename;
             if (path == ".")
-                scanfile(filename, include);
+                filename = *i;
+            else if (path == "/")
+                filename = "/" + *i;
             else
-                scanfile(path + "/" + filename, include);
+                filename = path + "/" + *i;
+            if (filter_rules.is_mergefile(metafile_path(filename))) {
+                if (verbose) {
+                    printf("Merging directory filter rules %s\n",
+                           filename.c_str());
+                }
+                try_merge_filter(filename, output_path);
+            }
         }
-    }
-}
 
-/* Include the specified file path in the backups.  Append the path to the
- * includes list, and to ensure that we actually see the path when scanning the
- * directory tree, add all the parent directories to the search list, which
- * means we will scan through the directory listing even if the files
- * themselves are excluded from being backed up. */
-void add_include(const char *path)
-{
-    /* Was an absolute path specified?  If so, we'll need to start scanning
-     * from the root directory.  Make sure that the user was consistent in
-     * providing either all relative paths or all absolute paths. */
-    if (path[0] == '/') {
-        if (includes.size() > 0 && relative_paths == true) {
-            fprintf(stderr,
-                    "Error: Cannot mix relative and absolute paths!\n");
-            exit(1);
+        /* Second pass: recursively scan all items in the directory for backup;
+         * scanfile() will check if the item should be included or not. */
+        for (vector<string>::iterator i = contents.begin();
+             i != contents.end(); ++i) {
+            const string& filename = *i;
+            if (path == ".")
+                scanfile(filename);
+            else if (path == "/")
+                scanfile("/" + filename);
+            else
+                scanfile(path + "/" + filename);
         }
 
-        relative_paths = false;
-
-        // Skip over leading '/'
-        path++;
-    } else if (relative_paths == false && path[0] != '/') {
-        fprintf(stderr, "Error: Cannot mix relative and absolute paths!\n");
-        exit(1);
-    }
-
-    includes.push_back(path);
-
-    /* Split the specified path into directory components, and ensure that we
-     * descend into all the directories along the path. */
-    const char *slash = path;
-
-    if (path[0] == '\0')
-        return;
-
-    while ((slash = strchr(slash + 1, '/')) != NULL) {
-        string component(path, slash - path);
-        searches.push_back(component);
+        filter_rules.restore();
     }
 }
 
@@ -660,8 +642,10 @@ void usage(const char *program)
         "  --dest=PATH          path where backup is to be written\n"
         "  --upload-script=COMMAND\n"
         "                       program to invoke for each backup file generated\n"
-        "  --exclude=PATH       exclude files in PATH from snapshot\n"
-        "  --exclude-name=NAME  exclude files called NAME from snapshot\n"
+        "  --exclude=PATTERN    exclude files matching PATTERN from snapshot\n"
+        "  --include=PATTERN    include files matching PATTERN in snapshot\n"
+        "  --dir-merge=PATTERN  parse files matching PATTERN to read additional\n"
+        "                       subtree-specific include/exclude rules during backup\n"
         "  --localdb=PATH       local backup metadata is stored in PATH\n"
         "  --tmpdir=PATH        path for temporarily storing backup files\n"
         "                           (defaults to TMPDIR environment variable or /tmp)\n"
@@ -686,6 +670,8 @@ void usage(const char *program)
 
 int main(int argc, char *argv[])
 {
+    hash_init();
+
     string backup_dest = "", backup_script = "";
     string localdb_dir = "";
     string backup_scheme = "";
@@ -698,18 +684,19 @@ int main(int argc, char *argv[])
     while (1) {
         static struct option long_options[] = {
             {"localdb", 1, 0, 0},           // 0
-            {"exclude", 1, 0, 0},           // 1
-            {"filter", 1, 0, 0},            // 2
-            {"filter-extension", 1, 0, 0},  // 3
-            {"dest", 1, 0, 0},              // 4
-            {"scheme", 1, 0, 0},            // 5
-            {"signature-filter", 1, 0, 0},  // 6
-            {"intent", 1, 0, 0},            // 7
-            {"full-metadata", 0, 0, 0},     // 8
-            {"tmpdir", 1, 0, 0},            // 9
-            {"upload-script", 1, 0, 0},     // 10
-            {"rebuild-statcache", 0, 0, 0}, // 11
-            {"exclude-name", 1, 0, 0},      // 12
+            {"filter", 1, 0, 0},            // 1
+            {"filter-extension", 1, 0, 0},  // 2
+            {"dest", 1, 0, 0},              // 3
+            {"scheme", 1, 0, 0},            // 4
+            {"signature-filter", 1, 0, 0},  // 5
+            {"intent", 1, 0, 0},            // 6
+            {"full-metadata", 0, 0, 0},     // 7
+            {"tmpdir", 1, 0, 0},            // 8
+            {"upload-script", 1, 0, 0},     // 9
+            {"rebuild-statcache", 0, 0, 0}, // 10
+            {"include", 1, 0, 0},           // 11
+            {"exclude", 1, 0, 0},           // 12
+            {"dir-merge", 1, 0, 0},         // 13
             // Aliases for short options
             {"verbose", 0, 0, 'v'},
             {NULL, 0, 0, 0},
@@ -726,46 +713,46 @@ int main(int argc, char *argv[])
             case 0:     // --localdb
                 localdb_dir = optarg;
                 break;
-            case 1:     // --exclude
-                if (optarg[0] != '/')
-                    excludes.push_back(optarg);
-                else
-                    excludes.push_back(optarg + 1);
-                break;
-            case 2:     // --filter
+            case 1:     // --filter
                 filter_program = optarg;
                 break;
-            case 3:     // --filter-extension
+            case 2:     // --filter-extension
                 filter_extension = optarg;
                 break;
-            case 4:     // --dest
+            case 3:     // --dest
                 backup_dest = optarg;
                 break;
-            case 5:     // --scheme
+            case 4:     // --scheme
                 backup_scheme = optarg;
                 break;
-            case 6:     // --signature-filter
+            case 5:     // --signature-filter
                 signature_filter = optarg;
                 break;
-            case 7:     // --intent
+            case 6:     // --intent
                 snapshot_intent = atof(optarg);
                 if (snapshot_intent <= 0)
                     snapshot_intent = 1;
                 break;
-            case 8:     // --full-metadata
+            case 7:     // --full-metadata
                 flag_full_metadata = true;
                 break;
-            case 9:     // --tmpdir
+            case 8:     // --tmpdir
                 tmp_dir = optarg;
                 break;
-            case 10:    // --upload-script
+            case 9:     // --upload-script
                 backup_script = optarg;
                 break;
-            case 11:    // --rebuild-statcache
+            case 10:    // --rebuild-statcache
                 flag_rebuild_statcache = true;
                 break;
-            case 12:     // --exclude-name
-                excluded_names.push_back(optarg);
+            case 11:    // --include
+                filter_rules.add_pattern(PathFilterList::INCLUDE, optarg, "");
+                break;
+            case 12:    // --exclude
+                filter_rules.add_pattern(PathFilterList::EXCLUDE, optarg, "");
+                break;
+            case 13:    // --dir-merge
+                filter_rules.add_pattern(PathFilterList::DIRMERGE, optarg, "");
                 break;
             default:
                 fprintf(stderr, "Unhandled long option!\n");
@@ -788,10 +775,6 @@ int main(int argc, char *argv[])
         return 1;
     }
 
-    searches.push_back(".");
-    for (int i = optind; i < argc; i++)
-        add_include(argv[i]);
-
     if (backup_dest == "" && backup_script == "") {
         fprintf(stderr,
                 "Error: Backup destination must be specified using --dest= or --upload-script=\n");
@@ -858,10 +841,11 @@ int main(int argc, char *argv[])
     metawriter = new MetadataWriter(tss, localdb_dir.c_str(), desc_buf,
                                     backup_scheme.c_str());
 
-    scanfile(".", false);
+    for (int i = optind; i < argc; i++) {
+        scanfile(argv[i]);
+    }
 
     ObjectReference root_ref = metawriter->close();
-    add_segment(root_ref.get_segment());
     string backup_root = root_ref.to_string();
 
     delete metawriter;
@@ -882,6 +866,7 @@ int main(int argc, char *argv[])
                                                    "checksums");
     FILE *checksums = fdopen(checksum_file->get_fd(), "w");
 
+    std::set<string> segment_list = db->GetUsedSegments();
     for (std::set<string>::iterator i = segment_list.begin();
          i != segment_list.end(); ++i) {
         string seg_path, seg_csum;