]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
BEE Backport CDP Plugin
authorHenrique <henrique.faria@baculasystems.com>
Tue, 24 Nov 2020 16:05:34 +0000 (17:05 +0100)
committerEric Bollengier <eric@baculasystems.com>
Thu, 24 Mar 2022 08:03:27 +0000 (09:03 +0100)
Author: Henrique <henrique.faria@baculasystems.com>
Date:   Mon Nov 18 23:46:16 2019 -0300

    cdp: added 'group' Plugin Parameter

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Wed Nov 13 02:23:37 2019 -0300

    cdp: added plugin param 'user' and fixed segfault on get_user_home_directory(...)

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Mon Nov 11 08:54:13 2019 -0300

    cdp: fix 5494 about a crash when a invalid userHome param is specified

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Fri Jul 26 15:52:21 2019 -0300

    cdp-plugin: tweak debug messages

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Fri Jul 26 15:17:22 2019 -0300

    cdp-plugin: tweak Journal Debug Level

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Fri Jul 26 14:51:09 2019 -0300

    cdp-plugin: tweak

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Wed Jul 24 15:48:35 2019 -0300

    cdp-plugin: tweak

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Tue Jul 16 21:32:04 2019 -0300

    cdp-plugin: fixed Unit Tests for Windows

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Mon Jul 15 22:13:51 2019 -0300

    cdp-fd-plugin:

    1) Fixed Unit Tests
    2) Fixed problem of double free during backup (Settings Record Spool Dir)
    3) Small refactoring

Author: Eric Bollengier <eric@baculasystems.com>
Date:   Wed Sep 25 11:44:09 2019 +0200

    cdp: Fix issue with context initialization

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Fri Jun 21 11:26:01 2019 -0300

    cdp: Fix #5085 about wrong windows backlashes

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Wed May 22 04:20:58 2019 -0300

    cdp-fd-plugin: changed the name of backed up files to a readable date format

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Wed May 22 03:44:45 2019 -0300

    cdp: fix #5066 about restore error on Windows

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Mon May 20 00:46:24 2019 -0300

    cdp-client: tweak debug messages

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Sun May 19 23:17:04 2019 -0300

    cdp: fix about file copy on Windows not working (Windows was keeping the file lock)

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Thu May 16 01:25:56 2019 -0300

    cdp: Fix #5049 about file change not being notified

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Tue May 14 13:33:38 2019 -0300

    cdp-fd-plugin: added compatibility with Windows

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Wed May 8 00:13:20 2019 -0300

    cdp-fd-plugin: changed name pattern of the file that's backed up by bacula

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Thu May 2 21:19:33 2019 -0300

    cdp: Fix #4991 about userHome usage

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Tue Apr 30 20:10:02 2019 -0300

    cdp-client: finished journal implementation and unit tests for Windows

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Thu Apr 25 22:58:24 2019 -0300

    cdp-client: added journal tests for Windows

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Tue Apr 16 15:46:08 2019 -0300

    cdp-fd-plugin: fixed userHome plugin parameter

Author: Eric Bollengier <eric@baculasystems.com>
Date:   Mon Apr 8 23:01:24 2019 +0200

    cdp: Fix backport

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Mon Apr 8 10:48:34 2019 -0300

    cdp-client: removed Qt Dependencies from the BackupService and the FolderWatcher

Author: Henrique <henrique.faria@baculasystems.com>
Date:   Thu Apr 4 22:15:00 2019 -0300

    cdp: Save the spool directory inside the journal. Configure the FileSet with the journal

    1) cdp-client: Changed BackupService to save the Spool Directory inside the Journal

    2) cdp-fd-plugin: Added code to adapt the FileSet based on the Journal's watched folders and the Spool Directory

Author: Henrique Faria <henrique.faria@baculasystems.com>
Date:   Tue Apr 2 09:59:59 2019 +0200

    cdp: Add CDP File Daemon plugin

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Apr 1 14:37:05 2019 -0300

        cdp-client: temporary #ifndefs in order to not break compilation for Windows

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Apr 1 11:03:40 2019 -0300

        cdp-client: added Qt4 connect() compatibility

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Apr 1 10:22:39 2019 -0300

        cdp-client: fixed Windows compilation

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Fri Mar 29 11:32:05 2019 -0300

        tray-monitor: added cdp-client

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Thu Mar 28 17:20:33 2019 -0300

        cdp-client: tweak UI text and code refactor

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Thu Mar 28 10:45:25 2019 -0300

        cdp-fd-plugin: fixed compilation

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Thu Mar 21 09:10:04 2019 +0100

        android: Update documentation and build scripts

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 20 22:56:37 2019 -0300

        tray-monitor: added CDP Client

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 20 15:26:27 2019 -0300

        cdp: Fixed Journal endTransaction() method

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 20 15:11:59 2019 -0300

        cdp: Added destructors to FileRecord and FolderRecord

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 20 14:28:39 2019 -0300

        cdp-client: fixed variable initialization of Journal Class

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 20 14:14:42 2019 -0300

        cdp:

        1-) Fixed variable initialization from Record Classes

        2-) Fixed tests

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 20 12:48:34 2019 -0300

        cdp-fd-plugin: changed createFile() function to let Bacula handle restore

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Tue Mar 19 22:58:02 2019 -0300

        cdp-fd-plugin: tweak

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Tue Mar 19 20:31:03 2019 -0300

        cdp-client: added command-line argument: debug level (-d)

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Tue Mar 19 18:27:16 2019 -0300

        cdp-client: Added a Journal Version to the Settings Record

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Tue Mar 19 17:17:55 2019 -0300

        cdp-client: upgraded command-line arguments

        1) Created gui and non-gui mode (arg '-c')
        2) Created optional arguments for Journal Path and Spool Dir

        Also, small refactor on backupservice.

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Mar 18 21:12:56 2019 -0300

        cdp-client: fixed memory leaks (FD Plugin)

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Mar 18 21:05:51 2019 -0300

        cdp-client: fixed memory leaks

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Mar 18 19:49:39 2019 -0300

        cdp-client: fixed error when copying big files to Spool Dir

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Sun Mar 17 14:07:05 2019 -0300

        cdp-client: Fixed Bugs:

        1) Watch from root would create invalid paths

        2) Fixed INotify error ENOENT when the path was valid

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Sat Mar 16 18:56:35 2019 -0300

        cdp-client: Added Error Dialog for errors with the FolderWatcher

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Thu Mar 14 19:17:17 2019 -0300

        cdp-client: changed FolderWatcher implementation to use INotify

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 13 18:15:11 2019 -0300

        cdp-client: tweak small fix

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 13 16:23:44 2019 -0300

        cdp-client: created GUI with add / remove folders functions

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 13 16:02:32 2019 -0300

        cdp-client: created removeFolderRecord() function in the Journal

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Tue Mar 12 00:07:42 2019 -0300

        cdp-client: fixed CDP FD Plugin after testing its execution with Bacula

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Sun Mar 10 20:47:50 2019 -0300

        cdp-client: tweak small renaming

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Fri Mar 8 03:32:17 2019 -0300

        cdp-client: changed journal path in command-line client

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Fri Mar 8 03:10:40 2019 -0300

        cdp-fd-plugin:

        1-) Created Plugin Parameter "userHome"
        2-) Fixed memory leaks
        3-) Code refactoring

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Thu Mar 7 12:16:36 2019 -0300

        cdp-client: fixed checkFile() on FD Plugin

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Thu Mar 7 00:49:05 2019 -0300

        cdp-client: FD Plugin now backups files from Spool Dir

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 6 10:27:00 2019 -0300

        tray-monitor: tweak (removed auto-generated files)

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Mar 6 00:02:06 2019 -0300

        cdp-client:

        1-) Added flock() to Journal calls
        2-) Added new Records: SettingsRecord and FolderRecord
        3-) Small refactors on the code

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Sun Mar 3 16:47:46 2019 -0300

        tray-monitor: fixed broken desktop compilation

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Mar 4 12:48:20 2019 -0300

        cdp-client: added flock() to Journal calls

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Mar 4 11:48:19 2019 -0300

        cdp-client: improved Journal

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Thu Feb 28 16:47:35 2019 -0300

        cdp-client: tweak

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Feb 27 22:12:43 2019 -0300

        cdp-client: tweak

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Feb 27 19:17:37 2019 -0300

        cdp-client: tweak

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Feb 27 18:02:40 2019 -0300

        cdp-client: tweak

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Tue Feb 26 12:07:24 2019 -0300

        cdp-client: tweak

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Tue Feb 26 11:38:45 2019 -0300

        cdp-client: tweak

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Mon Feb 25 22:30:19 2019 -0300

        cdp-client: created cdp-fdp-plugin (restore part)

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Fri Feb 22 03:07:45 2019 -0300

        cdp-client: created cdp-fdp-plugin (backup part)

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Sat Feb 16 14:51:44 2019 -0200

        cdp-client: created client executable

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Thu Feb 14 20:58:29 2019 -0200

        cdp-client: added backupservice

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Feb 13 13:54:58 2019 -0200

        cdp-client: added journal

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Wed Feb 13 13:53:20 2019 -0200

        cdp-client: tweak folder watcher

    Author: Henrique <henrique.faria@baculasystems.com>
    Date:   Fri Feb 8 12:43:34 2019 -0200

        cdp-client: created folder watcher

bacula/src/plugins/fd/cdp-fd.c [new file with mode: 0644]
bacula/src/plugins/fd/file-record.h [new file with mode: 0644]
bacula/src/plugins/fd/folder-record.h [new file with mode: 0644]
bacula/src/plugins/fd/journal.c [new file with mode: 0644]
bacula/src/plugins/fd/journal.h [new file with mode: 0644]
bacula/src/plugins/fd/settings-record.h [new file with mode: 0644]
bacula/src/tools/cdp-client/folderwatcher.cpp

diff --git a/bacula/src/plugins/fd/cdp-fd.c b/bacula/src/plugins/fd/cdp-fd.c
new file mode 100644 (file)
index 0000000..0d19736
--- /dev/null
@@ -0,0 +1,587 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2022 Kern Sibbald
+
+   The original author of Bacula is Kern Sibbald, with contributions
+   from many others, a complete list can be found in the file AUTHORS.
+
+   You may use this file and others of this release according to the
+   license defined in the LICENSE file, which includes the Affero General
+   Public License, v3.0 ("AGPLv3") and some additional permissions and
+   terms pursuant to its AGPLv3 Section 7.
+
+   This notice must be preserved when any source code is
+   conveyed and/or propagated.
+
+   Bacula(R) is a registered trademark of Kern Sibbald.
+*/
+
+#include "bacula.h"
+#include "fd_plugins.h"
+#include "fd_common.h"
+#include "lib/cmd_parser.h"
+#include "lib/mem_pool.h"
+#include "findlib/bfile.h"
+#include "journal.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define PLUGIN_LICENSE      "AGPLv3"
+#define PLUGIN_AUTHOR       "Henrique Faria"
+#define PLUGIN_DATE         "February 2019"
+#define PLUGIN_VERSION      "0.1"
+#define PLUGIN_DESCRIPTION  "CDP Plugin"
+
+#ifdef HAVE_WIN32
+#define CONCAT_PATH "%s\\%s"
+#define WORKING_JOURNAL_TEMPLATE "%s\\%s_%d.journal"
+#else
+#define CONCAT_PATH "%s/%s"
+#define WORKING_JOURNAL_TEMPLATE "%s/%s_%d.journal"
+#endif
+
+/* Forward referenced functions */
+static bRC newPlugin(bpContext *ctx);
+static bRC freePlugin(bpContext *ctx);
+static bRC handlePluginEvent(bpContext *ctx, bEvent *event, void *value);
+static bRC startBackupFile(bpContext *ctx, struct save_pkt *sp);
+static bRC endBackupFile(bpContext *ctx);
+static bRC pluginIO(bpContext *ctx, struct io_pkt *io);
+static bRC startRestoreFile(bpContext *ctx, const char *cmd);
+static bRC createFile(bpContext *ctx, struct restore_pkt *rp);
+static bRC endRestoreFile(bpContext *ctx);
+static bRC checkFile(bpContext *ctx, char *fname);
+
+/* Pointers to Bacula functions */
+static bFuncs *bfuncs = NULL;
+static bInfo  *binfo = NULL;
+
+/* Backup Variables */
+static char *working = NULL;
+
+static pInfo pluginInfo = {
+   sizeof(pluginInfo),
+   FD_PLUGIN_INTERFACE_VERSION,
+   FD_PLUGIN_MAGIC,
+   PLUGIN_LICENSE,
+   PLUGIN_AUTHOR,
+   PLUGIN_DATE,
+   PLUGIN_VERSION,
+   PLUGIN_DESCRIPTION
+};
+
+static pFuncs pluginFuncs = {
+   sizeof(pluginFuncs),
+   FD_PLUGIN_INTERFACE_VERSION,
+
+   /* Entry points into plugin */
+   newPlugin,                    /* new plugin instance */
+   freePlugin,                   /* free plugin instance */
+   NULL,
+   NULL,
+   handlePluginEvent,
+   startBackupFile,
+   endBackupFile,
+   startRestoreFile,
+   endRestoreFile,
+   pluginIO,
+   createFile,
+   NULL,
+   checkFile,
+   NULL,                         /* No ACL/XATTR */
+   NULL,                         /* No Restore file list */
+   NULL                          /* No checkStream */
+};
+
+static int DBGLVL = 50;
+
+class CdpContext: public SMARTALLOC
+{
+public:
+   bpContext *ctx;
+
+   /** Used by both Backup and Restore Cycles **/
+   BFILE fd;
+   POOLMEM *fname;
+   bool is_in_use;
+
+   /** Used only by the Backup Cycle **/
+   POOLMEM *clientJPath;
+   POOLMEM *jobJPath;
+   POOLMEM *drivesList; // Windows only
+   char *jobName;
+   
+   bool accurate_warning;
+   bool started_backup;
+   bool canceled;
+   alist userHomes;
+   alist journals;
+   int jIndex;
+   cmd_parser parser;
+   Journal *journal;
+
+   CdpContext(bpContext *actx):
+     ctx(actx), fname(NULL), is_in_use(false), clientJPath(NULL),
+     jobJPath(NULL), drivesList(NULL), jobName(NULL), accurate_warning(false),
+     started_backup(false), canceled(false),
+     userHomes(100, owned_by_alist), journals(100, not_owned_by_alist), jIndex(0)
+   {
+      fname = get_pool_memory(PM_FNAME);
+      clientJPath = get_pool_memory(PM_FNAME);
+      jobJPath = get_pool_memory(PM_FNAME);
+#ifdef HAVE_WIN32
+      drivesList = get_pool_memory(PM_FNAME);
+      *drivesList = 0;
+#endif
+      *fname = *clientJPath = *jobJPath = 0;
+   };
+
+   /** Methods called during Backup */
+   void migrateJournal() {
+      char *uh;
+      int i = 0;
+
+      foreach_alist(uh, &userHomes) {
+         Journal *j = new Journal();
+         Mmsg(clientJPath, CONCAT_PATH, uh, JOURNAL_CLI_FNAME);
+         j->setJournalPath(clientJPath);
+         Mmsg(jobJPath, WORKING_JOURNAL_TEMPLATE, working, jobName, i);
+         j->migrateTo(jobJPath);
+         journals.append(j);
+         i++;
+      }
+   };
+
+   bool handleBackupCommand(bpContext *ctx, char *cmd) {
+      int i;
+      POOLMEM *userHome;
+      parser.parse_cmd(cmd);
+      for (i = 1; i < parser.argc ; i++) {
+
+         if (strcasecmp(parser.argk[i], "userhome") == 0 && parser.argv[i]) {
+            userHome = get_pool_memory(PM_FNAME);
+            pm_strcpy(userHome, parser.argv[i]);
+            struct stat sp;
+
+            if (stat(userHome, &sp) != 0) {
+               Jmsg(ctx, M_ERROR, _("Parameter userhome not found: %s\n"), userHome);
+               return false;
+            }
+
+            if (!S_ISDIR(sp.st_mode)) {
+               Jmsg(ctx, M_ERROR, _("Paramater userhome is not a directory: %s\n"), userHome);
+               return false;
+            }
+
+            Dmsg(ctx, DBGLVL, "User Home: %s\n", userHome);
+            userHomes.append(bstrdup(userHome));
+            free_and_null_pool_memory(userHome);
+         } else if (strcasecmp(parser.argk[i], "user") == 0 && parser.argv[i]) {
+            userHome = get_pool_memory(PM_FNAME);
+            int rc = get_user_home_directory(parser.argv[i], userHome);
+
+            if (rc != 0) {
+               Jmsg(ctx, M_ERROR, _("User not found in the system: %s\n"), parser.argv[i]);
+               return false;
+            }
+
+            userHomes.append(bstrdup(userHome));
+            Dmsg(ctx, DBGLVL, "User Home: %s\n", userHome);
+            free_and_null_pool_memory(userHome);
+            return true;
+         } else if (strcasecmp(parser.argk[i], "group") == 0 && parser.argv[i]) {
+            int rc = get_home_directories(parser.argv[i], &userHomes);
+
+            if (rc != 0) {
+               return false;
+            }
+
+            return true;
+     } else {
+            Jmsg(ctx, M_ERROR, _("Can't analyse plugin command line %s\n"), cmd);
+            return false;
+         }
+      }
+
+      return true;
+   };
+
+   FileRecord *nextRecord() {
+      if (canceled) {
+         if (journal) {
+            journal->endTransaction();
+         }
+         return NULL;
+      }
+
+      if (!started_backup) { 
+         if (jIndex >= journals.size()) {
+            return NULL;
+         }
+
+         journal = (Journal *) journals[jIndex];
+
+         if (!journal->beginTransaction("r")) {
+            return NULL;
+         }
+
+         started_backup = true;
+      }
+
+      FileRecord *fc = journal->readFileRecord();
+
+      if (fc == NULL) {
+         journal->endTransaction();
+         started_backup = false;
+         unlink(journal->_jPath);
+         Dmsg(ctx, DBGLVL, "No more files to backup. Deleting journal: %s\n", journal->_jPath);
+         delete(journal);
+         jIndex++;
+      }
+
+      return fc;
+   };
+
+   ~CdpContext() {
+      if (journal) {
+         // Clear any possible pending transaction (e.g canceled job)
+         journal->endTransaction();
+         canceled = true;
+      }
+
+      free_and_null_pool_memory(clientJPath);
+      free_and_null_pool_memory(jobJPath);
+      free_and_null_pool_memory(fname);
+#ifdef HAVE_WIN32
+      free_and_null_pool_memory(drivesList);
+#endif
+   };
+
+   /* Adjust the current fileset depending on what we find in the Journal */
+   void adapt(Journal *j) {
+      SettingsRecord *settings = j->readSettings();
+
+      /* We should not backup the Spool Directory */
+      if (settings != NULL) {
+         char *sdir = bstrdup(settings->getSpoolDir());
+         bfuncs->AddExclude(ctx, sdir);
+         Dmsg(ctx, DBGLVL, "Excluded Spool Directory from FileSet %s\n", sdir);
+         delete settings;
+      }
+
+      /* Foreach folder watched, we add the folder to the backup */
+      if (!j->beginTransaction("r")) {
+         return;
+      }
+
+      FolderRecord *rec;
+
+#ifdef HAVE_WIN32
+      int i = 0;
+
+      for(;;) {
+         rec = j->readFolderRecord();
+
+         if (rec == NULL) {
+            drivesList[i] = '\0';
+            break;
+         }
+
+         /*On Windows, we must also add the folder drives to create
+           the VSS Snapshot */
+         if (!strchr(drivesList, rec->path[0])) {
+            drivesList[i++] = toupper(rec->path[0]);
+            Dmsg(ctx, DBGLVL, "Included Drive %c\n", rec->path[0]);
+         }
+         
+         bfuncs->AddInclude(ctx, rec->path);
+         Dmsg(ctx, DBGLVL, "Included Directory into the FileSet %s\n", rec->path);
+         delete rec;
+      }
+
+#else 
+      for(;;) {
+         rec = j->readFolderRecord();
+
+         if (rec == NULL) {
+            break;
+         }
+
+         bfuncs->AddInclude(ctx, rec->path);
+         Dmsg(ctx, DBGLVL, "Included Directory %s\n", rec->path);
+         delete rec;
+      }
+#endif
+
+      j->endTransaction();
+   };
+
+   void adaptFileSet() {
+      for (int i = 0; i < journals.size(); i++) {
+         Journal *j = (Journal *) journals[i];
+         adapt(j);
+      }
+
+   }
+};
+
+/*
+ * Plugin called here when it is first loaded
+ */
+bRC DLL_IMP_EXP
+loadPlugin(bInfo *lbinfo, bFuncs *lbfuncs, pInfo **pinfo, pFuncs **pfuncs)
+{
+   bfuncs = lbfuncs;                  /* set Bacula funct pointers */
+   binfo  = lbinfo;
+
+   *pinfo  = &pluginInfo;             /* return pointer to our info */
+   *pfuncs = &pluginFuncs;            /* return pointer to our functions */
+
+   bfuncs->getBaculaValue(NULL, bVarWorkingDir, (void *)&working);
+   return bRC_OK;
+}
+
+/*
+ * Plugin called here when it is unloaded, normally when
+ *  Bacula is going to exit.
+ */
+bRC DLL_IMP_EXP
+unloadPlugin()
+{
+   return bRC_OK;
+}
+
+/*
+ * Called here to make a new instance of the plugin -- i.e. when
+ *  a new Job is started.  There can be multiple instances of
+ *  each plugin that are running at the same time.  Your
+ *  plugin instance must be thread safe and keep its own
+ *  local data.
+ */
+static bRC newPlugin(bpContext *ctx)
+{
+  CdpContext *pCtx = New(CdpContext(ctx));
+  ctx->pContext = (void *) pCtx;        /* set our context pointer */
+  Dmsg(ctx, DBGLVL, "Working Directory: %s\n", working);
+  return bRC_OK;
+}
+
+/*
+ * Release everything concerning a particular instance of a
+ *  plugin. Normally called when the Job terminates.
+ */
+static bRC freePlugin(bpContext *ctx)
+{
+   CdpContext *pCtx = (CdpContext *) ctx->pContext;
+   delete(pCtx);
+   return bRC_OK;
+}
+
+static bRC handlePluginEvent(bpContext *ctx, bEvent *event, void *value)
+{
+   CdpContext *pCtx = (CdpContext *) ctx->pContext;
+
+   switch (event->eventType) {
+
+   case bEventPluginCommand:
+      if (!pCtx->handleBackupCommand(ctx, (char *) value)) {
+         return bRC_Error;
+      };
+      pCtx->is_in_use = true;
+      pCtx->migrateJournal();
+      pCtx->adaptFileSet();
+      break;
+
+   case bEventEstimateCommand:
+      Jmsg(ctx, M_FATAL, _("The CDP plugin doesn't support estimate\n"));
+      return bRC_Error;
+
+   case bEventJobStart:
+      bfuncs->getBaculaValue(NULL, bVarJobName, (void *) &(pCtx->jobName));
+      
+      if (pCtx->jobName == NULL) {
+         pCtx->jobName = (char *) "backup_job";
+      }
+
+      Dmsg(ctx, DBGLVL, "Job Name: %s\n", pCtx->jobName);
+      break;
+
+   case bEventCancelCommand:
+      pCtx->canceled = true;
+      Dmsg(ctx, DBGLVL, "Job canceled\n");
+      break;
+
+#ifdef HAVE_WIN32
+   case bEventVssPrepareSnapshot:
+      strcpy((char *) value, pCtx->drivesList);
+      Dmsg(ctx, DBGLVL, "VSS Drives list: %s\n", pCtx->drivesList);
+      break; 
+#endif
+
+   default:
+      break;
+   }
+
+   return bRC_OK;
+}
+
+/*
+ * Called when starting to backup a file.  Here the plugin must
+ *  return the "stat" packet for the directory/file and provide
+ *  certain information so that Bacula knows what the file is.
+ *  The plugin can create "Virtual" files by giving them a
+ *  name that is not normally found on the file system.
+ */
+static bRC startBackupFile(bpContext *ctx, struct save_pkt *sp)
+{
+   CdpContext *pCtx = (CdpContext *) ctx->pContext;
+   FileRecord *rec = pCtx->nextRecord();
+
+   if(rec != NULL) {
+      //Fill save_pkt struct
+      POOLMEM *bacula_fname = get_pool_memory(PM_FNAME);
+      rec->getBaculaName(bacula_fname);
+      sp->fname = bstrdup(bacula_fname);
+      sp->type = FT_REG;
+      rec->decode_attrs(sp->statp);
+      
+      //Save the name of the file that's inside the Spool Dir
+      //That's the file that will be backed up
+      pm_strcpy(pCtx->fname, rec->sname);
+      delete(rec);
+      free_and_null_pool_memory(bacula_fname);
+      Dmsg(ctx, DBGLVL, "Starting backup of file: %s\n", sp->fname);
+      return bRC_OK;
+   } else {
+      return bRC_Stop;
+   }
+
+}
+
+/*
+ * Done backing up a file.
+ */
+static bRC endBackupFile(bpContext *ctx)
+{
+   return bRC_More;
+}
+
+/*
+ * Do actual I/O.  Bacula calls this after startBackupFile
+ *   or after startRestoreFile to do the actual file
+ *   input or output.
+ */
+static bRC pluginIO(bpContext *ctx, struct io_pkt *io)
+{
+   CdpContext *pCtx = (CdpContext *) ctx->pContext;
+
+   io->status = -1;
+   io->io_errno = 0;
+
+   if (!pCtx) {
+      return bRC_Error;
+   }
+
+   switch (io->func) {
+   case IO_OPEN:
+      if (bopen(&pCtx->fd, pCtx->fname, io->flags, io->mode) < 0) {
+            io->io_errno = errno;
+            io->status = -1;
+            Jmsg(ctx, M_ERROR, "Open file %s failed: ERR=%s\n",
+                 pCtx->fname, strerror(errno));
+            return bRC_Error;
+      }
+      io->status = 1;
+      break;
+
+   case IO_READ:
+      if (!is_bopen(&pCtx->fd)) {
+         Jmsg(ctx, M_FATAL, "Logic error: NULL read FD\n");
+         return bRC_Error;
+      }
+
+      /* Read data from file */
+      io->status = bread(&pCtx->fd, io->buf, io->count);
+      break;
+
+   case IO_WRITE:
+      if (!is_bopen(&pCtx->fd)) {
+         Jmsg(ctx, M_FATAL, "Logic error: NULL write FD\n");
+         return bRC_Error;
+      }
+
+      io->status = bwrite(&pCtx->fd, io->buf, io->count);
+      break;
+
+   case IO_SEEK:
+      if (!is_bopen(&pCtx->fd)) {
+         Jmsg(ctx, M_FATAL, "Logic error: NULL FD on delta seek\n");
+         return bRC_Error;
+      }
+      /* Seek not needed for this plugin, we don't use real sparse file */
+      io->status = blseek(&pCtx->fd, io->offset, io->whence);
+      break;
+
+   /* Cleanup things during close */
+   case IO_CLOSE:
+      io->status = bclose(&pCtx->fd);
+      break;
+   }
+
+   return bRC_OK;
+}
+
+static bRC startRestoreFile(bpContext *ctx, const char *cmd)
+{
+   Dmsg(ctx, DBGLVL, "Started file restoration\n");
+   return bRC_Core;
+}
+
+/*
+ * Called here to give the plugin the information needed to
+ *  re-create the file on a restore.  It basically gets the
+ *  stat packet that was created during the backup phase.
+ *  This data is what is needed to create the file, but does
+ *  not contain actual file data.
+ */
+static bRC createFile(bpContext *ctx, struct restore_pkt *rp)
+{
+   CdpContext *pCtx = (CdpContext *) ctx->pContext;
+   pm_strcpy(pCtx->fname, rp->ofname);
+   rp->create_status = CF_CORE;
+   Dmsg(ctx, DBGLVL, "Creating file %s\n", rp->ofname);
+   return bRC_OK;
+}
+
+static bRC endRestoreFile(bpContext *ctx)
+{
+   Dmsg(ctx, DBGLVL, "Finished file restoration\n");
+   return bRC_OK;
+}
+
+/* When using Incremental dump, all previous dumps are necessary */
+static bRC checkFile(bpContext *ctx, char *fname)
+{
+   CdpContext *pCtx = (CdpContext *) ctx->pContext;
+
+   if (pCtx->is_in_use) {
+      if (!pCtx->accurate_warning) {
+         pCtx->accurate_warning = true;
+         Jmsg(ctx, M_WARNING, "Accurate mode is not supported. Please disable Accurate mode for this job.\n");
+      }
+
+      return bRC_Seen;
+   } else {
+      return bRC_OK;
+   }
+}
+
+
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/bacula/src/plugins/fd/file-record.h b/bacula/src/plugins/fd/file-record.h
new file mode 100644 (file)
index 0000000..6909a8f
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2022 Kern Sibbald
+
+   The original author of Bacula is Kern Sibbald, with contributions
+   from many others, a complete list can be found in the file AUTHORS.
+
+   You may use this file and others of this release according to the
+   license defined in the LICENSE file, which includes the Affero General
+   Public License, v3.0 ("AGPLv3") and some additional permissions and
+   terms pursuant to its AGPLv3 Section 7.
+
+   This notice must be preserved when any source code is
+   conveyed and/or propagated.
+
+   Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+#ifndef journalfilerecord_H
+#define journalfilerecord_H
+
+#include "bacula.h"
+#include <string.h>
+
+/**
+ * Implementation is part of Bacula's findlib module, and is on:
+ * "/src/findlib/attribs.c"
+ */
+extern int decode_stat(char *buf, struct stat *statp, int stat_size, int32_t *LinkFI);
+extern void encode_stat(char *buf, struct stat *statp, int stat_size, int32_t LinkFI, int data_stream);
+
+/**
+ * @brief Data that is saved and retrieved by using the @class Journal
+ */
+class FileRecord
+{
+   public:
+      char *name;
+      char *sname;
+      char *fattrs;
+      int64_t mtime;
+
+      FileRecord():
+         name(NULL), sname(NULL), fattrs(NULL), mtime(0)
+      {}
+
+      bool encode_attrs() {
+         struct stat statbuf;
+#ifndef HAVE_WIN32
+         if(lstat(this->name, &statbuf) != 0) {
+            return false;
+         }
+
+         this->mtime = (int64_t) statbuf.st_mtime;
+         this->fattrs = (char *) malloc(500 * sizeof(char));
+         encode_stat(this->fattrs, &statbuf, sizeof(statbuf), 0, 0); 
+#else
+         FILE *fp = fopen(this->name, "r");
+
+         if (!fp) {
+            Dmsg1(0, "Could not open file %s\n", this->name);
+            return false;
+         }
+
+         int fd = _fileno(fp);
+
+         if(fstat(fd, &statbuf) != 0) {
+            fclose(fp);
+            Dmsg1(0, "Could not encode attributes of file %s\n", this->name);
+            return false;
+         }
+
+         this->mtime = (int64_t) statbuf.st_mtime;
+         this->fattrs = (char *) malloc(500 * sizeof(char));
+         encode_stat(this->fattrs, &statbuf, sizeof(statbuf), 0, 0); 
+         fclose(fp);
+#endif
+         return true;
+      }
+
+      void decode_attrs(struct stat &sbuf) {
+         int32_t lfi;
+         decode_stat(this->fattrs, &sbuf, sizeof(sbuf), &lfi);
+      }
+
+      bool equals(const FileRecord *rec) {
+         return strcmp(this->name, rec->name) == 0
+            && strcmp(this->fattrs, rec->fattrs) == 0
+            && this->mtime == rec->mtime;
+      }
+
+      void getBaculaName(POOLMEM *target) {
+         char mtime_date[200];
+         time_t t = (time_t) mtime;
+         struct tm *timeinfo = localtime(&t);
+         strftime(mtime_date, 200, "%Y%m%d_%H%M%S", timeinfo);
+         Mmsg(target, "%s.%s", name, mtime_date);
+      }
+
+      ~FileRecord() {
+         if (name != NULL) {
+            free(name);
+         }
+
+         if (sname != NULL) {
+            free(sname);
+         }
+
+         if (fattrs != NULL) {
+            free(fattrs);
+         }
+      }
+};
+
+#endif
diff --git a/bacula/src/plugins/fd/folder-record.h b/bacula/src/plugins/fd/folder-record.h
new file mode 100644 (file)
index 0000000..f3565a4
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2022 Kern Sibbald
+
+   The original author of Bacula is Kern Sibbald, with contributions
+   from many others, a complete list can be found in the file AUTHORS.
+
+   You may use this file and others of this release according to the
+   license defined in the LICENSE file, which includes the Affero General
+   Public License, v3.0 ("AGPLv3") and some additional permissions and
+   terms pursuant to its AGPLv3 Section 7.
+
+   This notice must be preserved when any source code is
+   conveyed and/or propagated.
+
+   Bacula(R) is a registered trademark of Kern Sibbald.
+*/
+
+#ifndef folder_record_H
+#define folder_record_H
+
+#include "bacula.h"
+#include <string.h>
+
+/**
+ * @brief Data that is saved and retrieved by using the @class Journal
+ */
+class FolderRecord
+{
+public:
+    char *path;
+
+    FolderRecord():
+        path(NULL)
+    {}
+
+    ~FolderRecord() {
+        if (path != NULL) {
+            free(path);
+        }
+
+    }
+};
+
+#endif
diff --git a/bacula/src/plugins/fd/journal.c b/bacula/src/plugins/fd/journal.c
new file mode 100644 (file)
index 0000000..d246725
--- /dev/null
@@ -0,0 +1,760 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2022 Kern Sibbald
+
+   The original author of Bacula is Kern Sibbald, with contributions
+   from many others, a complete list can be found in the file AUTHORS.
+
+   You may use this file and others of this release according to the
+   license defined in the LICENSE file, which includes the Affero General
+   Public License, v3.0 ("AGPLv3") and some additional permissions and
+   terms pursuant to its AGPLv3 Section 7.
+
+   This notice must be preserved when any source code is
+   conveyed and/or propagated.
+
+   Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+#include "journal.h"
+
+static int DBGLVL = 90;
+
+bool Journal::beginTransaction(const char *mode)
+{
+   if (hasTransaction) {
+      return true;
+   }
+
+   bool hasLock = false;
+   int timeout = 1800; 
+
+   for (int time = 0; time < timeout; time++) {
+      _fp = bfopen(_jPath, mode);
+
+      if (!_fp) {
+         Dmsg0(0, "Tried to start transaction but Journal File was not found.\n");
+         return false;
+      }
+
+      _fd = fileno(_fp);
+      int rc = flock(_fd, LOCK_EX | LOCK_NB);
+
+      //Success. Lock acquired.
+      if (rc == 0) {
+         hasLock = true;
+         break;
+      }
+
+      fclose(_fp);
+      sleep(1);
+   }
+
+   if (hasLock) {
+      hasTransaction = true;
+      return true;
+   } else {
+      Dmsg0(0, "Tried to start transaction but could not lock Journal File.\n");
+      return false;
+   }
+}
+
+void Journal::endTransaction()
+{
+   if (!hasTransaction) {
+      return;
+   }
+
+   if (_fp != NULL) {
+      int rc = flock(_fd, LOCK_UN);
+
+      if (rc != 0) {
+         Dmsg0(0, "could not release flock\n");
+      }
+
+      fclose(_fp);
+      _fp = NULL;
+   }
+
+   _fd = -1;
+   hasTransaction = false;
+}
+
+/**
+ * Given a string formatted as 'key=val\n',
+ * this function tries to return 'val'
+ */
+char *Journal::extract_val(const char *key_val)
+{
+   const int SANITY_CHECK = 10000;
+   int max_idx = cstrlen(key_val) - 1;
+   char *val = (char *) malloc(SANITY_CHECK * sizeof(char));
+
+   int idx_keyend = 0;
+
+   while(key_val[idx_keyend] != '=') {
+      idx_keyend++;
+
+      if(idx_keyend > max_idx) {
+         free(val);
+         return NULL;
+      }
+   }
+
+   int i;
+   int j = 0;
+   for(i = idx_keyend + 1; key_val[i] != '\n' ; i++) {
+      val[j] = key_val[i];
+      j++;
+
+      if(i > max_idx) {
+         free(val);
+         return NULL;
+      }
+   }
+
+   val[j] = '\0';
+   return val;
+}
+
+bool Journal::setJournalPath(const char *path)
+{
+   _jPath = bstrdup(path);
+   FILE *jfile = bfopen(_jPath, "r");    
+
+   if (!jfile) {
+      if (this->beginTransaction("w")) {
+         SettingsRecord rec;
+         rec.journalVersion = JOURNAL_VERSION;
+         this->writeSettings(rec);
+      } else {
+         Dmsg1(0, "(ERROR) Could not create Journal File: %s\n", path);
+         return false;
+      }
+   } else {
+      fclose(jfile);
+   }
+
+   return true;
+}
+
+bool Journal::setJournalPath(const char *path, const char *spoolDir)
+{
+   _jPath = bstrdup(path);
+   FILE *jfile = fopen(_jPath, "r");
+
+   if (!jfile) {
+      if (this->beginTransaction("w")) {
+         SettingsRecord rec;
+         rec.journalVersion = JOURNAL_VERSION;
+         rec.setSpoolDir(spoolDir);
+         this->writeSettings(rec);
+      } else {
+         Dmsg1(0, "(ERROR) Could not create Journal File: %s\n", path);
+         return false;
+      }
+   } else {
+      fclose(jfile);
+   }
+
+   return true;
+}
+
+
+bool Journal::writeSettings(SettingsRecord &rec)
+{
+   int rc;
+   bool success = true;
+   const char *spoolDir;
+   char jversion[50];
+   char heartbeat[50];
+
+   if(!this->beginTransaction("r+")) {
+      Dmsg0(50, "Could not start transaction for writeSettings()\n");
+      success = false;
+      goto bail_out;
+   }
+
+   spoolDir = rec.getSpoolDir();
+   
+   if (spoolDir == NULL) {
+      spoolDir = "<NULL>";
+   } 
+
+   edit_int64(rec.heartbeat, heartbeat);
+   edit_int64(rec.journalVersion, jversion);
+   rc = fprintf(_fp,
+         "Settings {\n"
+         "spooldir=%s\n"
+         "heartbeat=%s\n"
+         "jversion=%s\n"
+         "}\n",
+         spoolDir,
+         heartbeat,
+         jversion);
+
+   if(rc < 0) {
+      success = false;
+      Dmsg1(50, "(ERROR) Could not write SettingsRecord. RC=%d\n", rc);
+      goto bail_out;
+   }
+
+   Dmsg3(DBGLVL, 
+         "WROTE RECORD:\n"
+         " Settings {\n"
+         "  spooldir=%s\n"
+         "  heartbeat=%s\n"
+         "  jversion=%s\n"
+         " }\n",
+         spoolDir,
+         heartbeat,
+         jversion);
+
+bail_out:
+   this->endTransaction();
+   return success;
+}
+
+SettingsRecord *Journal::readSettings()
+{
+   const int SANITY_CHECK = 10000;
+   bool corrupted = false;
+   char tmp[SANITY_CHECK];
+   char jversion[SANITY_CHECK];
+   char heartbeat[SANITY_CHECK];
+   char spoolpath[SANITY_CHECK];
+   char *jvstr = NULL;
+   char *hbstr = NULL;
+   SettingsRecord *rec = NULL;
+
+   if(!this->beginTransaction("r+")) {
+      Dmsg0(0, "Could not start transaction for readSettings()\n");
+      goto bail_out;
+   }
+
+   //reads line "Settings {\n"
+   if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+   rec = new SettingsRecord();
+
+   //reads Spool Dir
+   if(!bfgets(spoolpath, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+   rec->setSpoolDir(extract_val(spoolpath));
+   if(rec->getSpoolDir() == NULL) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+   //reads Heartbeat
+   if(!bfgets(heartbeat, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+   hbstr = extract_val(heartbeat);
+   if(hbstr == NULL) {
+      corrupted = true;
+      goto bail_out;
+   }
+   rec->heartbeat = atoi(hbstr);
+
+   //reads Journal Version
+   if(!bfgets(jversion, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+   jvstr = extract_val(jversion);
+   if(jvstr == NULL) {
+      corrupted = true;
+      goto bail_out;
+   }
+   rec->journalVersion = atoi(jvstr);
+
+   //reads line "}\n"
+   if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+   Dmsg3(DBGLVL,
+         "READ RECORD:\n"
+         " Settings {\n"
+         "  spooldir=%s\n"
+         "  heartbeat=%s\n"
+         "  jversion=%s\n"
+         " }\n",
+         rec->getSpoolDir(),
+         hbstr,
+         jvstr);
+
+bail_out:
+   if(jvstr != NULL) {
+      free(jvstr);
+   }
+
+   if(hbstr != NULL) {
+      free(hbstr);
+   }
+
+   if(rec != NULL && rec->getSpoolDir() != NULL && strcmp(rec->getSpoolDir(), "<NULL>") == 0) {
+      free(rec->getSpoolDir());
+      rec->setSpoolDir(NULL);
+   }
+
+   if(corrupted) {
+      Dmsg0(0, "Could not read Settings Record. Journal is Corrupted.\n");
+
+      if(rec != NULL) {
+         delete rec;
+         rec = NULL;
+      }
+   }
+
+   this->endTransaction();
+   return rec;
+}
+
+bool Journal::writeFileRecord(const FileRecord &record)
+{
+   int rc;
+   bool success = true;
+   char mtime_str[50];
+
+   if(!this->beginTransaction("a")) {
+      success = false;
+      Dmsg0(0, "Could not start transaction for writeFileRecord()\n");
+      goto bail_out;
+   }
+
+   edit_int64(record.mtime, mtime_str);
+   rc = fprintf(_fp,
+         "File {\n"
+         "name=%s\n"
+         "sname=%s\n"
+         "mtime=%s\n"
+         "attrs=%s\n"
+         "}\n",
+         record.name,
+         record.sname,
+         mtime_str,
+         record.fattrs);
+
+   if(rc < 0) {
+      success = false;
+      Dmsg1(50, "(ERROR) Could not write FileRecord. RC=%d\n", rc);
+      goto bail_out;
+   }
+
+   Dmsg4(DBGLVL,
+         "NEW RECORD:\n"
+         " File {\n"
+         "  name=%s\n"
+         "  sname=%s\n"
+         "  mtime=%s"
+         "  attrs=%s\n"
+         " }\n",
+         record.name,
+         record.sname,
+         mtime_str,
+         record.fattrs);
+
+bail_out:
+   this->endTransaction();
+   return success;
+}
+
+FileRecord *Journal::readFileRecord()
+{
+   const int SANITY_CHECK = 10000;
+   bool corrupted = false;
+   char tmp[SANITY_CHECK];
+   char fname[SANITY_CHECK];
+   char sname[SANITY_CHECK];
+   char fattrs[SANITY_CHECK];
+   char mtime[SANITY_CHECK];
+   char *mstr = NULL;
+   FileRecord *rec = NULL;
+
+   if(!hasTransaction) {
+      Dmsg0(0, "(ERROR) Journal::readFileRecord() called without any transaction\n");
+      goto bail_out;
+   }
+
+   //Reads lines until it finds a FileRecord, or until EOF
+   for(;;) {
+      if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+         goto bail_out;
+      }
+
+      if(strstr(tmp, "File {\n") != NULL) {
+         break;
+      }
+   }
+
+   rec = new FileRecord();
+
+   //reads filename
+   if(!bfgets(fname, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+   rec->name = extract_val(fname);
+   if(rec->name == NULL) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+   //reads spoolname
+   if(!bfgets(sname, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+   rec->sname = extract_val(sname);
+   if(rec->sname == NULL) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+   //reads mtime
+   if(!bfgets(mtime, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+   mstr = extract_val(mtime);
+   if(mstr == NULL) {
+      corrupted = true;
+      goto bail_out;
+   }
+   rec->mtime = atoi(mstr);
+
+   //reads encoded attributes
+   if(!bfgets(fattrs, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+   rec->fattrs = extract_val(fattrs);
+   if(rec->fattrs == NULL) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+   Dmsg4(DBGLVL, 
+         "READ RECORD:\n"
+         " File {\n"
+         "  name=%s\n"
+         "  sname=%s\n"
+         "  mtime=%s\n"
+         "  attrs=%s\n"
+         " }\n",
+         rec->name,
+         rec->sname,
+         mstr,
+         rec->fattrs);
+
+   //reads line "}\n"
+   if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+bail_out:
+   if(mstr != NULL) {
+      free(mstr);
+   }
+
+   if(corrupted) {
+      Dmsg0(0, "Could not read File Record. Journal is Corrupted.\n");
+
+      if(rec != NULL) {
+         delete rec;
+         rec = NULL;
+      }
+   }
+
+   return rec;
+}
+
+bool Journal::writeFolderRecord(const FolderRecord &record)
+{
+   int rc;
+   bool success = true;
+
+   if(!this->beginTransaction("a")) {
+      success = false;
+      Dmsg0(0, "Could not start transaction for writeFileRecord()\n");
+      goto bail_out;
+   }
+
+   rc = fprintf(_fp,
+         "Folder {\n"
+         "path=%s\n"
+         "}\n",
+         record.path);
+
+   if(rc < 0) {
+      success = false;
+      Dmsg1(0, "(ERROR) Could not write FolderRecord. RC=%d\n", rc);
+      goto bail_out;
+   }
+
+   Dmsg1(DBGLVL,
+         "NEW RECORD:\n"
+         " Folder {\n"
+         "  path=%s\n"
+         " }\n",
+         record.path);
+
+bail_out:
+   this->endTransaction();
+   return success;
+}
+
+FolderRecord *Journal::readFolderRecord()
+{
+   const int SANITY_CHECK = 10000;
+   bool corrupted = false;
+   char tmp[SANITY_CHECK];
+   char path[SANITY_CHECK];
+   FolderRecord *rec = NULL;
+
+   if(!hasTransaction) {
+      Dmsg0(0, "(ERROR) Journal::readFolderRecord() called without any transaction\n");
+      goto bail_out;
+   }
+
+   //Reads lines until it finds a FolderRecord, or until EOF
+   for(;;) {
+      if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+         //No need to set 'corrupted = true' here
+         goto bail_out;
+      }
+
+      if(strstr(tmp, "Folder {\n") != NULL) {
+         break;
+      }
+   }
+
+   rec = new FolderRecord();
+
+   //reads folder path
+   if(!bfgets(path, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+   rec->path = extract_val(path);
+   if(rec->path == NULL) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+   Dmsg1(DBGLVL, 
+         "READ RECORD:\n"
+         " Folder {\n"
+         "  path=%s\n"
+         " }\n",
+         rec->path);
+
+   //reads line "}\n"
+   if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+      corrupted = true;
+      goto bail_out;
+   }
+
+bail_out:
+   if(corrupted) {
+      Dmsg0(0, "Could not read FolderRecord. Journal is Corrupted.\n");
+
+      if(rec != NULL) {
+         delete rec;
+         rec = NULL;
+      }
+   }
+
+   return rec;
+}
+
+bool Journal::removeFolderRecord(const char* folder)
+{
+   bool success = false;
+   const int SANITY_CHECK = 10000;
+   char path[SANITY_CHECK];
+   char tmp[SANITY_CHECK];
+   char *recPath;
+   FILE *tmpFp = NULL;
+   int rc;
+
+   POOL_MEM tmp_jPath;
+   Mmsg(tmp_jPath, "%s.temp", _jPath);
+
+   if(!this->beginTransaction("r")) {
+      goto bail_out;
+   }
+
+   tmpFp = bfopen(tmp_jPath.c_str(), "w");
+
+   if(tmpFp == NULL) {
+      goto bail_out;
+   }
+
+   for(;;) {
+      if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+         goto bail_out;
+      }
+
+      if(strstr(tmp, "Folder {\n") != NULL) {
+         //reads folder path
+         if(!bfgets(path, SANITY_CHECK, _fp)) {
+            goto bail_out;
+         }
+
+         recPath = extract_val(path);
+
+         if(recPath == NULL) {
+            goto bail_out;
+         }
+
+         //reads line "}\n"
+         if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+            goto bail_out;
+         }
+
+         if(bstrcmp(folder, recPath) == 0) {
+            //Didn't found the Record that needs to be removed
+            rc = fprintf(tmpFp,
+                  "Folder {\n"
+                  "path=%s\n"
+                  "}\n",
+                  recPath);
+
+            if(rc < 0) {
+               goto bail_out;
+            }
+         } else {
+            //Found the Folder Record that needs to be removed
+            success = true;
+         }
+
+      } else {
+         fprintf(tmpFp, "%s", tmp);
+      }
+   }
+
+bail_out:
+
+   if(tmpFp != NULL) {
+      fclose(tmpFp);
+   }
+
+   if(success) {
+      fclose(_fp);
+      _fp = NULL;
+      unlink(_jPath);
+      rc = rename(tmp_jPath.c_str(), _jPath);
+
+      if(rc != 0) {
+         Dmsg0(0, "Could not rename TMP Journal\n");
+      }
+   }
+
+   this->endTransaction();
+   return success;
+}
+
+bool Journal::migrateTo(const char *newPath)
+{
+   bool success = true;
+   const int SANITY_CHECK = 10000;
+   char tmp[SANITY_CHECK];
+   FILE *tmpFp = NULL;
+   FILE *newFp = NULL;
+   int rc;
+
+   POOLMEM *tmp_jPath = get_pool_memory(PM_FNAME);
+   Mmsg(tmp_jPath, "%s.temp", newPath);
+
+   if(!this->beginTransaction("r")) {
+      success = false;
+      goto bail_out;
+   }
+
+   Dmsg2(DBGLVL, "Migrating Journal %s to %s...\n", _jPath, newPath);
+   tmpFp = bfopen(tmp_jPath, "w");
+   newFp = bfopen(newPath, "w");
+
+   if (tmpFp == NULL) { 
+      Dmsg1(0, "Could not bfopen %s. Aborting migration.\n", tmp_jPath);
+      success = false;
+      goto bail_out;
+   }
+
+   if (newFp == NULL) {
+      Dmsg1(0, "Could not bfopen %s. Aborting migration.\n", newPath);
+      success = false;
+      goto bail_out;
+   }
+
+   // Migrate everything to the new Journal ('newFp')
+   // Remove FileRecords from the old Journal
+   // by not saving them in 'tmpFp'
+   for(;;) {
+      if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+         break;
+      }
+
+      if(strstr(tmp, "File {") != NULL) {
+         //Found a FileRecord
+         //Write it only in the New Jornal
+         fprintf(newFp, "%s", tmp);
+
+         for(int i = 0; i < 5; i++) {
+            if(!bfgets(tmp, SANITY_CHECK, _fp)) {
+               //Found a corrupted FileRecord
+               Dmsg0(0, "Found a corrupt FileRecord. Canceling Migration");
+               success = false;
+               goto bail_out;
+            }
+
+            fprintf(newFp, "%s", tmp);
+         }
+
+      } else {
+         fprintf(newFp, "%s", tmp);
+         fprintf(tmpFp, "%s", tmp);
+      }
+   }
+
+bail_out:
+
+   if(newFp != NULL) {
+      fclose(newFp);
+   }
+
+   if(tmpFp != NULL) {
+      fclose(tmpFp);
+   }
+
+   if(success) {
+      fclose(_fp);
+      _fp = NULL;
+      unlink(_jPath);
+      rc = rename(tmp_jPath, _jPath);
+
+      if(rc != 0) {
+         Dmsg0(0, "Could not rename TMP Journal\n");
+      }
+
+      free(_jPath);
+      _jPath = bstrdup(newPath);
+      Dmsg0(DBGLVL, "Journal migration completed\n");
+   }
+
+   free_and_null_pool_memory(tmp_jPath);
+   this->endTransaction();
+   return success;
+}
diff --git a/bacula/src/plugins/fd/journal.h b/bacula/src/plugins/fd/journal.h
new file mode 100644 (file)
index 0000000..76b9516
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2022 Kern Sibbald
+
+   The original author of Bacula is Kern Sibbald, with contributions
+   from many others, a complete list can be found in the file AUTHORS.
+
+   You may use this file and others of this release according to the
+   license defined in the LICENSE file, which includes the Affero General
+   Public License, v3.0 ("AGPLv3") and some additional permissions and
+   terms pursuant to its AGPLv3 Section 7.
+
+   This notice must be preserved when any source code is
+   conveyed and/or propagated.
+
+   Bacula(R) is a registered trademark of Kern Sibbald.
+*/
+
+#ifndef journal_H
+#define journal_H
+
+#include "settings-record.h"
+#include "folder-record.h"
+#include "file-record.h"
+
+#ifndef HAVE_WIN32
+#include <sys/file.h>
+#endif
+
+#ifdef HAVE_WIN32
+#define JOURNAL_CLI_FNAME "bcdp-cli.journal"
+#else
+#define JOURNAL_CLI_FNAME ".bcdp-cli.journal"
+#endif
+
+#define JOURNAL_VERSION 1
+
+/**
+ * @brief The Journal persists and retrieves @class FileRecord objects.
+ *
+ * Used by:
+ *
+ * 1-) The CDP Client, to store information about files that
+ * should backed up.
+ *
+ * 2-) The CDP FD Plugin, to decide which file should be backed
+ * up on a specific Job.
+ *
+ * The current implementation uses a plain text file to store the records.
+ */
+class Journal
+{
+
+private:
+    FILE * _fp;
+    int _fd;
+
+public:
+    char *_jPath;
+    bool hasTransaction;
+
+    Journal():
+        _fp(NULL), _fd(-1), _jPath(NULL), hasTransaction(false)
+    {}
+
+    ~Journal() {}
+
+    bool setJournalPath(const char *path);
+    bool setJournalPath(const char *path, const char *spoolDir);
+    bool migrateTo(const char* newPath);
+
+    bool beginTransaction(const char *mode);
+    void endTransaction();
+
+    bool writeSettings(SettingsRecord &record);
+    SettingsRecord *readSettings();
+
+    bool writeFileRecord(const FileRecord &record);
+    FileRecord *readFileRecord();
+
+    bool removeFolderRecord(const char *folder);
+    bool writeFolderRecord(const FolderRecord &record);
+    FolderRecord *readFolderRecord();
+
+    /** Public only because it's used by Unit Tests */
+    char *extract_val(const char *key_val);
+
+    //TODO: warnSizeFull();
+    //TODO: removeRecord()
+    //TODO: pruneRecords()
+};
+
+#endif
diff --git a/bacula/src/plugins/fd/settings-record.h b/bacula/src/plugins/fd/settings-record.h
new file mode 100644 (file)
index 0000000..e7da2ca
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2022 Kern Sibbald
+
+   The original author of Bacula is Kern Sibbald, with contributions
+   from many others, a complete list can be found in the file AUTHORS.
+
+   You may use this file and others of this release according to the
+   license defined in the LICENSE file, which includes the Affero General
+   Public License, v3.0 ("AGPLv3") and some additional permissions and
+   terms pursuant to its AGPLv3 Section 7.
+
+   This notice must be preserved when any source code is
+   conveyed and/or propagated.
+
+   Bacula(R) is a registered trademark of Kern Sibbald.
+*/
+
+#ifndef settingsrecord_H
+#define settingsrecord_H
+
+#include "bacula.h"
+#include <string.h>
+
+/**
+ * @brief Data that is saved and retrieved by using the @class Journal
+ */
+class SettingsRecord
+{
+private:
+    char *spoolDir;
+
+public:
+    int64_t heartbeat;
+    int64_t journalVersion;
+
+    const char *getSpoolDir() {
+        return spoolDir;
+    }
+
+    void setSpoolDir(const char *sdir) {
+        if (sdir == NULL) {
+            return;
+        }
+
+        spoolDir = bstrdup(sdir);
+    }
+
+    SettingsRecord():
+        spoolDir(NULL), heartbeat(-1), journalVersion(-1)
+    {}
+
+    ~SettingsRecord() {}
+};
+
+#endif
index 044b8e68d0ae99741bebd93b9dd61b2e6516193a..23adfd54b0c6e31e737d681aa84deb1833a0ee1a 100644 (file)
@@ -68,7 +68,7 @@ POOLMEM *FolderWatcher::watch(const char *folder)
    if (rc) {
       POOLMEM *err_msg = get_pool_memory(PM_EMSG);
       Mmsg(err_msg, "Error: could not start INotify Thread."
-            "Please, contact Bacula Systems.\n");
+            "Please, contact Bacula Support.\n");
       return err_msg;
    }
 
@@ -116,7 +116,7 @@ POOLMEM *FolderWatcher::watchDirRecursive(const char *dir)
             break;
 
          default:
-            Mmsg(err_msg, "Unknown Error. Please contact Bacula Systems.");
+            Mmsg(err_msg, "Unknown Error. Please contact Bacula Support.");
             break;
       }
 
@@ -411,7 +411,7 @@ POOLMEM *FolderWatcher::watch(const char *folder)
       _dirHandle = NULL;
       POOLMEM *err_msg = get_pool_memory(PM_EMSG);
       Mmsg(err_msg, "Error: could not create handle for folder %s. "
-            "Please, contact Bacula Systems.\n", folder);
+            "Please, contact Bacula Support.\n", folder);
       return err_msg;
    }
 
@@ -421,7 +421,7 @@ POOLMEM *FolderWatcher::watch(const char *folder)
    if (rc) {
       POOLMEM *err_msg = get_pool_memory(PM_EMSG);
       Mmsg(err_msg, "Error: could not start Watcher Thread. "
-            "Please, contact Bacula Systems.\n");
+            "Please, contact Bacula Support.\n");
       return err_msg;
    }