--- /dev/null
+/*
+ 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
+
--- /dev/null
+/*
+ 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
--- /dev/null
+/*
+ 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
--- /dev/null
+/*
+ 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;
+}
--- /dev/null
+/*
+ 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
--- /dev/null
+/*
+ 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
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;
}
break;
default:
- Mmsg(err_msg, "Unknown Error. Please contact Bacula Systems.");
+ Mmsg(err_msg, "Unknown Error. Please contact Bacula Support.");
break;
}
_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;
}
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;
}