]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
pluginlib: Create plugin base framework for FD Plugins.
authorRadosław Korzeniewski <radoslaw@korzeniewski.net>
Thu, 16 Sep 2021 11:22:33 +0000 (13:22 +0200)
committerEric Bollengier <eric@baculasystems.com>
Thu, 14 Sep 2023 11:56:56 +0000 (13:56 +0200)
bacula/src/plugins/fd/pluginlib/pluginbase.cpp [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/pluginbase.h [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/pluginclass.cpp [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/pluginclass.h [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/pluginctx.cpp [new file with mode: 0644]
bacula/src/plugins/fd/pluginlib/pluginctx.h [new file with mode: 0644]

diff --git a/bacula/src/plugins/fd/pluginlib/pluginbase.cpp b/bacula/src/plugins/fd/pluginlib/pluginbase.cpp
new file mode 100644 (file)
index 0000000..6813829
--- /dev/null
@@ -0,0 +1,372 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2023 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.
+ */
+/**
+ * @file pluginbase.cpp
+ * @author Radosław Korzeniewski (radoslaw@korzeniewski.net)
+ * @brief This is a Bacula File Daemon generic plugin interface.
+ * @version 1.0.0
+ * @date 2021-04-08
+ *
+ * @copyright Copyright (c) 2021 All rights reserved. IP transferred to Bacula Systems according to agreement.
+ */
+
+#include "pluginbase.h"
+#include <sys/stat.h>
+#include <signal.h>
+#include <sys/select.h>
+
+/*
+ * libbac uses its own sscanf implementation which is not compatible with
+ * libc implementation, unfortunately.
+ * use bsscanf for Bacula sscanf flavor
+ */
+#ifdef sscanf
+#undef sscanf
+#endif
+
+#define pluginclass(ctx)     pluginlib::PLUGINBCLASS *self = (pluginlib::PLUGINBCLASS*)ctx->pContext;
+
+/* Forward referenced functions */
+static bRC newPlugin(bpContext *ctx);
+static bRC freePlugin(bpContext *ctx);
+static bRC getPluginValue(bpContext *ctx, pVariable var, void *value);
+static bRC setPluginValue(bpContext *ctx, pVariable var, void *value);
+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 endRestoreFile(bpContext *ctx);
+static bRC createFile(bpContext *ctx, struct restore_pkt *rp);
+static bRC setFileAttributes(bpContext *ctx, struct restore_pkt *rp);
+static bRC plugincheckFile(bpContext *ctx, char *fname);
+static bRC handleXACLdata(bpContext *ctx, struct xacl_pkt *xacl);
+static bRC queryParameter(bpContext *ctx, struct query_pkt *qp);
+static bRC metadataRestore(bpContext *ctx, struct meta_pkt *mp);
+
+/* Pointers to Bacula functions */
+bFuncs *bfuncs = NULL;
+bInfo *binfo = NULL;
+
+static pFuncs pluginFuncs =
+{
+   sizeof(pluginFuncs),
+   FD_PLUGIN_INTERFACE_VERSION,
+
+   /* Entry points into plugin */
+   newPlugin,
+   freePlugin,
+   getPluginValue,
+   setPluginValue,
+   handlePluginEvent,
+   startBackupFile,
+   endBackupFile,
+   startRestoreFile,
+   endRestoreFile,
+   pluginIO,
+   createFile,
+   setFileAttributes,
+   plugincheckFile,
+   handleXACLdata,
+   NULL,                        /* No restore file list */
+   NULL,                        /* No checkStream */
+   queryParameter,
+   metadataRestore,
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Plugin Information structure */
+static pInfo pluginInfo = {
+   sizeof(pluginInfo),
+   FD_PLUGIN_INTERFACE_VERSION,
+   FD_PLUGIN_MAGIC,
+   PLUGIN_LICENSE,
+   PLUGIN_AUTHOR,
+   PLUGIN_DATE,
+   PLUGIN_VERSION,
+   PLUGIN_DESCRIPTION,
+};
+
+/*
+ * 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 function pointers */
+   binfo = lbinfo;
+
+   Dmsg4(DINFO, "%s Plugin version %s%s %s\n", PLUGINNAME, PLUGIN_VERSION, VERSIONGIT_STR, PLUGIN_DATE);
+
+   *pinfo = &pluginInfo;           /* return pointer to our info */
+   *pfuncs = &pluginFuncs;         /* return pointer to our functions */
+
+   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;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+
+/*
+ * 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)
+{
+   pluginlib::PLUGINBCLASS *self = (pluginlib::PLUGINBCLASS*)new_plugin_factory(ctx);
+
+   if (!self) {
+      DMSG1(ctx, DERROR, "newPlugin: Cannot build plugin %s!\n", PLUGINNAME);
+      return bRC_Error;
+   }
+
+   DMSG(ctx, DINFO, "newPlugin: %s\n", PLUGINNAME);
+   ctx->pContext = (void *)self;
+
+   pthread_t mythid = pthread_self();
+   DMSG2(ctx, DVDEBUG, "pContext = %p thid = %p\n", self, mythid);
+
+   /* setup plugin */
+   self->setup_plugin(ctx);
+
+   return bRC_OK;
+}
+
+/*
+ * Release everything concerning a particular instance of
+ *  a plugin. Normally called when the Job terminates.
+ */
+static bRC freePlugin(bpContext *ctx)
+{
+   ASSERT_CTX;
+
+   pluginclass(ctx);
+   DMSG(ctx, D1, "freePlugin this=%p\n", self);
+   if (!self){
+      return bRC_Error;
+   }
+   delete self;
+
+   return bRC_OK;
+}
+
+/*
+ * Called by core code to get a variable from the plugin.
+ *   Not currently used.
+ */
+static bRC getPluginValue(bpContext *ctx, pVariable var, void *value)
+{
+   ASSERT_CTX;
+
+   DMSG0(ctx, D3, "getPluginValue called.\n");
+   pluginclass(ctx);
+   return self->getPluginValue(ctx,var, value);
+}
+
+/*
+ * Called by core code to set a plugin variable.
+ *  Not currently used.
+ */
+static bRC setPluginValue(bpContext *ctx, pVariable var, void *value)
+{
+   ASSERT_CTX;
+
+   DMSG0(ctx, D3, "setPluginValue called.\n");
+   pluginclass(ctx);
+   return self->setPluginValue(ctx, var, value);
+}
+
+/*
+ * Called by Bacula when there are certain events that the
+ *   plugin might want to know.  The value depends on the
+ *   event.
+ */
+static bRC handlePluginEvent(bpContext *ctx, bEvent *event, void *value)
+{
+   ASSERT_CTX;
+   if (!event) {
+      return bRC_Error;
+   }
+
+   pthread_t mythid = pthread_self();
+   pluginclass(ctx);
+   DMSG3(ctx, D1, "handlePluginEvent (%i) pContext = %p thid = %p\n", event->eventType, self, mythid);
+   return self->handlePluginEvent(ctx, event, value);
+}
+
+/*
+ * 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)
+{
+   ASSERT_CTX;
+   if (!sp) {
+      return bRC_Error;
+   }
+
+   DMSG0(ctx, D1, "startBackupFile.\n");
+   pluginclass(ctx);
+   return self->startBackupFile(ctx, sp);
+}
+
+/*
+ * Done backing up a file.
+ */
+static bRC endBackupFile(bpContext *ctx)
+{
+   ASSERT_CTX;
+
+   DMSG0(ctx, D1, "endBackupFile.\n");
+   pluginclass(ctx);
+   return self->endBackupFile(ctx);
+}
+
+/*
+ * Called when starting restore the file, right after a createFile().
+ */
+static bRC startRestoreFile(bpContext *ctx, const char *cmd)
+{
+   ASSERT_CTX;
+
+   DMSG1(ctx, D1, "startRestoreFile: %s\n", NPRT(cmd));
+   pluginclass(ctx);
+   return self->startRestoreFile(ctx, cmd);
+}
+
+/*
+ * Done restore the file.
+ */
+static bRC endRestoreFile(bpContext *ctx)
+{
+   ASSERT_CTX;
+
+   DMSG0(ctx, D1, "endRestoreFile.\n");
+   pluginclass(ctx);
+   return self->endRestoreFile(ctx);
+}
+
+/*
+ * 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)
+{
+   ASSERT_CTX;
+
+   DMSG0(ctx, DVDEBUG, "pluginIO.\n");
+   pluginclass(ctx);
+   return self->pluginIO(ctx, io);
+}
+
+/*
+ * 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)
+{
+   ASSERT_CTX;
+
+   DMSG0(ctx, D1, "createFile.\n");
+   pluginclass(ctx);
+   return self->createFile(ctx, rp);
+}
+
+/*
+ * Called after the file has been restored. This can be used to
+ *  set directory permissions, ...
+ */
+static bRC setFileAttributes(bpContext *ctx, struct restore_pkt *rp)
+{
+   ASSERT_CTX;
+
+   DMSG0(ctx, D1, "setFileAttributes.\n");
+   pluginclass(ctx);
+   return self->setFileAttributes(ctx, rp);
+}
+
+/*
+ * handleXACLdata used for ACL/XATTR backup and restore
+ */
+static bRC handleXACLdata(bpContext *ctx, struct xacl_pkt *xacl)
+{
+   ASSERT_CTX;
+
+   DMSG(ctx, D1, "handleXACLdata: %i\n", xacl->func);
+   pluginclass(ctx);
+   return self->handleXACLdata(ctx, xacl);
+}
+
+/*
+ * QueryParameter interface
+ */
+static bRC queryParameter(bpContext *ctx, struct query_pkt *qp)
+{
+   ASSERT_CTX;
+
+   DMSG2(ctx, D1, "queryParameter: cmd:%s param:%s\n", qp->command, qp->parameter);
+   pluginclass(ctx);
+   return self->queryParameter(ctx, qp);
+}
+
+/*
+ * Metadata Restore interface
+ */
+static bRC metadataRestore(bpContext *ctx, struct meta_pkt *mp)
+{
+   ASSERT_CTX;
+
+   DMSG2(ctx, D1, "metadataRestore: %d %d\n", mp->total_size, mp->type);
+   pluginclass(ctx);
+   return self->metadataRestore(ctx, mp);
+}
+
+/*
+ * checkFile used for accurate mode backup
+ */
+static bRC plugincheckFile(bpContext * ctx, char *fname)
+{
+   ASSERT_CTX;
+
+   DMSG(ctx, D3, "checkFile for: %s\n", fname);
+   pluginclass(ctx);
+   return self->checkFile(ctx, fname);
+}
diff --git a/bacula/src/plugins/fd/pluginlib/pluginbase.h b/bacula/src/plugins/fd/pluginlib/pluginbase.h
new file mode 100644 (file)
index 0000000..f1f9fcc
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2023 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.
+ */
+/**
+ * @file pluginbase.cpp
+ * @author Radosław Korzeniewski (radoslaw@korzeniewski.net)
+ * @brief This is a Bacula File Daemon generic plugin interface.
+ * @version 1.0.0
+ * @date 2021-04-08
+ *
+ * @copyright Copyright (c) 2021 All rights reserved. IP transferred to Bacula Systems according to agreement.
+ */
+
+#include "pluginclass.h"
+#define USE_CMD_PARSER
+#include "fd_common.h"
+
+
+#ifndef PLUGINLIB_PLUGINBASE_H
+#define PLUGINLIB_PLUGINBASE_H
+
+// Plugin Info definitions
+extern const char *PLUGIN_LICENSE;
+extern const char *PLUGIN_AUTHOR;
+extern const char *PLUGIN_DATE;
+extern const char *PLUGIN_VERSION;
+extern const char *PLUGIN_DESCRIPTION;
+
+// Plugin linking time variables
+extern const char *PLUGINPREFIX;
+extern const char *PLUGINNAME;
+extern const char *PLUGINNAMESPACE;
+extern const bool CUSTOMNAMESPACE;
+extern const char *BACKEND_CMD;
+
+// custom checkFile() callback
+typedef bRC (*checkFile_t)(bpContext *ctx, char *fname);
+extern checkFile_t checkFile;
+
+// the list of valid plugin options
+extern const char *valid_params[];
+
+struct metadataTypeMap
+{
+   const char *command;
+   metadata_type type;
+};
+
+pluginlib::PLUGINBCLASS *new_plugin_factory(bpContext *ctx);
+
+#endif // PLUGINLIB_PLUGINBASE_H
diff --git a/bacula/src/plugins/fd/pluginlib/pluginclass.cpp b/bacula/src/plugins/fd/pluginlib/pluginclass.cpp
new file mode 100644 (file)
index 0000000..9431363
--- /dev/null
@@ -0,0 +1,768 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2023 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.
+ */
+/**
+ * @file pluginclass.cpp
+ * @author Radosław Korzeniewski (radoslaw@korzeniewski.net)
+ * @brief This is a Bacula File Daemon general plugin framework. The Class.
+ * @version 1.0.0
+ * @date 2021-04-08
+ *
+ * @copyright Copyright (c) 2021 All rights reserved. IP transferred to Bacula Systems according to agreement.
+ */
+
+#include "pluginclass.h"
+#include <sys/stat.h>
+#include <signal.h>
+#include <sys/select.h>
+
+/*
+ * libbac uses its own sscanf implementation which is not compatible with
+ * libc implementation, unfortunately.
+ * use bsscanf for Bacula sscanf flavor
+ */
+#ifdef sscanf
+#undef sscanf
+#endif
+
+// synchronie access to job_cancelled variable
+//   smart_lock<smart_mutex> lg(&mutex); - removed on request
+#define CHECK_JOB_CANCELLED \
+      { \
+         if (job_cancelled) { \
+            return bRC_Error; \
+         } \
+      }
+
+namespace pluginlib
+{
+   /**
+    * @brief The main plugin setup method executed
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    */
+   void PLUGINBCLASS::setup_plugin(bpContext *ctx)
+   {
+      DMSG0(ctx, DINFO, "PLUGINCLASS::setup_plugin\n");
+
+      getBaculaVar(bVarJobId, (void *)&JobId);
+      DMSG(ctx, D1, "bVarJobId: %d\n", JobId);
+
+      char *varpath;
+      getBaculaVar(bVarExePath, (void *)&varpath);
+      DMSG(ctx, DINFO, "bVarExePath: %s\n", varpath);
+
+      pm_strcpy(execpath, varpath);
+      strip_trailing_slashes(execpath.c_str());
+      DMSG(ctx, DINFO, "ExePath: %s\n", execpath.c_str());
+
+      getBaculaVar(bVarWorkingDir, (void *)&varpath);
+      DMSG(ctx, DINFO, "bVarWorkingDir: %s\n", varpath);
+
+      pm_strcpy(workingpath, varpath);
+      strip_trailing_slashes(workingpath.c_str());
+      DMSG(ctx, DINFO, "WorkingPath: %s\n", workingpath.c_str());
+   }
+
+   /**
+    * @brief This is the main method for handling events generated by Bacula.
+    *          The behavior of the method depends on event type generated, but there are
+    *          some events which does nothing, just return with bRC_OK. Every event is
+    *          tracked in debug trace file to verify the event flow during development.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param event a Bacula event structure
+    * @param value optional event value
+    * @return bRC bRC_OK - in most cases signal success/no error
+    *             bRC_Error - in most cases signal error
+    *             <other> - depend on Bacula Plugin API if applied
+    */
+   bRC PLUGINBCLASS::handlePluginEvent(bpContext *ctx, bEvent *event, void *value)
+   {
+      // extract original plugin context, basically it should be `this`
+      PLUGINBCLASS *pctx = (PLUGINBCLASS *)ctx->pContext;
+
+      CHECK_JOB_CANCELLED;
+
+      switch (event->eventType)
+      {
+      case bEventJobStart:
+         DMSG(ctx, D3, "bEventJobStart value=%s\n", NPRT((char *)value));
+         // getBaculaVar(bVarJobId, (void *)&JobId);
+         getBaculaVar(bVarJobName, (void *)&JobName);
+         // if (CUSTOMPREVJOBNAME){
+         //    getBaculaVar(bVarPrevJobName, (void *)&prevjobname);
+         // }
+         return perform_jobstart(ctx);
+
+      case bEventJobEnd:
+         DMSG(ctx, D3, "bEventJobEnd value=%s\n", NPRT((char *)value));
+         return perform_jobend(ctx);
+
+      case bEventLevel:
+         char lvl;
+         lvl = (char)((intptr_t) value & 0xff);
+         DMSG(ctx, D2, "bEventLevel='%c'\n", lvl);
+         switch (lvl) {
+            case 'F':
+               DMSG0(ctx, D2, "backup level = Full\n");
+               mode = BackupFull;
+               break;
+            case 'I':
+               DMSG0(ctx, D2, "backup level = Incr\n");
+               mode = BackupIncr;
+               break;
+            case 'D':
+               DMSG0(ctx, D2, "backup level = Diff\n");
+               mode = BackupDiff;
+               break;
+            default:
+               // TODO: handle other backup levels
+               DMSG0(ctx, D2, "unsupported backup level!\n");
+               return bRC_Error;
+         }
+         return perform_joblevel(ctx, lvl);
+
+      case bEventSince:
+         since = (time_t) value;
+         DMSG(ctx, D2, "bEventSince=%ld\n", (intptr_t) since);
+         return perform_jobsince(ctx);
+
+      case bEventStartBackupJob:
+         DMSG(ctx, D3, "bEventStartBackupJob value=%s\n", NPRT((char *)value));
+         return perform_backupjobstart(ctx);
+
+      case bEventEndBackupJob:
+         DMSG(ctx, D2, "bEventEndBackupJob value=%s\n", NPRT((char *)value));
+         return perform_backupjobend(ctx);
+
+      case bEventStartRestoreJob:
+         DMSG(ctx, DINFO, "StartRestoreJob value=%s\n", NPRT((char *)value));
+         getBaculaVar(bVarWhere, &where);
+         DMSG(ctx, DINFO, "Where=%s\n", NPRT(where));
+         getBaculaVar(bVarRegexWhere, &regexwhere);
+         DMSG(ctx, DINFO, "RegexWhere=%s\n", NPRT(regexwhere));
+         getBaculaVar(bVarReplace, &replace);
+         DMSG(ctx, DINFO, "Replace=%c\n", replace);
+         mode = Restore;
+         return perform_restorejobstart(ctx);
+
+      case bEventEndRestoreJob:
+         DMSG(ctx, DINFO, "bEventEndRestoreJob value=%s\n", NPRT((char *)value));
+         return perform_restorejobend(ctx);
+
+      /* Plugin command e.g. plugin = <plugin-name>:parameters */
+      case bEventEstimateCommand:
+         DMSG(ctx, D1, "bEventEstimateCommand value=%s\n", NPRT((char *)value));
+         switch (mode)
+         {
+         case BackupIncr:
+            mode = EstimateIncr;
+            break;
+         case BackupDiff:
+            mode = EstimateDiff;
+            break;
+         default:
+#if __cplusplus >= 201703L
+            [[fallthrough]];
+#endif
+         case BackupFull:
+            mode = EstimateFull;
+            break;
+         }
+         return prepare_estimate(ctx, (char *)value);
+
+      /* Plugin command e.g. plugin = <plugin-name>:parameters */
+      case bEventBackupCommand:
+         DMSG(ctx, D2, "bEventBackupCommand value=%s\n", NPRT((char *)value));
+         // pluginconfigsent = false;
+         return prepare_backup(ctx, (char*)value);
+
+      /* Plugin command e.g. plugin = <plugin-name>:parameters */
+      case bEventRestoreCommand:
+         DMSG(ctx, D2, "bEventRestoreCommand value=%s\n", NPRT((char *)value));
+         return prepare_restore(ctx, (char*)value);
+
+      /* Plugin command e.g. plugin = <plugin-name>:parameters */
+      case bEventPluginCommand:
+         DMSG(ctx, D2, "bEventPluginCommand value=%s\n", NPRT((char *)value));
+         // getBaculaVar(bVarAccurate, (void *)&accurate_mode);
+         // if (isourplugincommand(PLUGINPREFIX, (char*)value) && !backend_available)
+         // {
+         //    DMSG2(ctx, DERROR, "Unable to use backend: %s Err=%s\n", backend_cmd.c_str(), backend_error.c_str());
+         //    JMSG2(ctx, M_FATAL, "Unable to use backend: %s Err=%s\n", backend_cmd.c_str(), backend_error.c_str());
+         //    return bRC_Error;
+         // }
+         return prepare_command(ctx, (char*)value);
+
+      case bEventOptionPlugin:
+#if __cplusplus >= 201703L
+            [[fallthrough]];
+#endif
+      case bEventHandleBackupFile:
+         if (isourplugincommand(PLUGINPREFIX, (char*)value)){
+            DMSG0(ctx, DERROR, "Invalid handle Option Plugin called!\n");
+            JMSG2(ctx, M_FATAL,
+                  "The %s plugin doesn't support the Option Plugin configuration.\n"
+                  "Please review your FileSet and move the Plugin=%s"
+                  "... command into the Include {} block.\n",
+                  PLUGINNAME, PLUGINPREFIX);
+            return bRC_Error;
+         }
+         break;
+
+      case bEventEndFileSet:
+         DMSG(ctx, D3, "bEventEndFileSet value=%s\n", NPRT((char *)value));
+         return perform_jobendfileset(ctx);
+
+      case bEventRestoreObject:
+         /* Restore Object handle - a plugin configuration for restore and user supplied parameters */
+         if (!value){
+            DMSG0(ctx, DINFO, "End restore objects\n");
+            break;
+         }
+         DMSG(ctx, D2, "bEventRestoreObject value=%p\n", value);
+         return parse_plugin_restore_object(ctx, (restore_object_pkt *) value);
+
+      case bEventCancelCommand:
+         DMSG2(ctx, D3, "bEventCancelCommand self = %p pctx = %p\n", this, pctx);
+         pctx->job_cancelled = true;
+         return pctx->perform_cancel_command(ctx);
+
+      default:
+         // enabled only for Debug
+         DMSG2(ctx, D2, "Unknown event: %s (%d) \n", eventtype2str(event), event->eventType);
+      }
+
+      return bRC_OK;
+   }
+
+   /**
+    * @brief Parsing a plugin command.
+    *
+    * @param ctx bpContext - Bacula Plugin context structure
+    * @param command plugin command string to parse
+    * @param params output parsed params list
+    * @return bRC bRC_OK - on success, bRC_Error - on error
+    */
+   bRC PLUGINBCLASS::parse_plugin_command(bpContext *ctx, const char *command)
+   {
+      // bool found;
+      // int count;
+      // int parargc, argc;
+
+      DMSG(ctx, DINFO, "Parse command: %s\n", command);
+      if (parser.parse_cmd(command) != bRC_OK) {
+         DMSG0(ctx, DERROR, "Unable to parse Plugin command line.\n");
+         JMSG0(ctx, M_FATAL, "Unable to parse Plugin command line.\n");
+         return bRC_Error;
+      }
+
+      /* switch pluginctx to the required context or allocate a new one */
+      pluginctx_switch_command(command);
+
+      /* the first (zero) parameter is a plugin name, we should skip it */
+      for (int i = 1; i < parser.argc; i++)
+      {
+         /* scan for abort_on_error parameter */
+         if (strcasecmp(parser.argk[i], "abort_on_error") == 0){
+            /* found, so check the value if provided, I only check the first char */
+            if (parser.argv[i] && *parser.argv[i] == '0') {
+               pluginctx_clear_abort_on_error();
+            } else {
+               pluginctx_set_abort_on_error();
+            }
+            DMSG1(ctx, DINFO, "abort_on_error found: %s\n", pluginctx_is_abort_on_error() ? "True" : "False");
+         }
+         /* scan for listing parameter, so the estimate job should be executed as a Listing procedure */
+         if (EstimateFull == mode && bstrcmp(parser.argk[i], "listing")){
+            /* we have a listing parameter which for estimate means .ls command */
+            mode = Listing;
+            DMSG0(ctx, DINFO, "listing procedure param found\n");
+            continue;
+         }
+         /* scan for query parameter, so the estimate job should be executed as a QueryParam procedure */
+         if (EstimateFull == mode && strcasecmp(parser.argk[i], "query") == 0){
+            /* found, so check the value if provided */
+            if (parser.argv[i]){
+               mode = QueryParams;
+               DMSG0(ctx, DINFO, "query procedure param found\n");
+            }
+         }
+         /* handle it with pluginctx */
+         bRC status = pluginctx_parse_parameter(ctx, parser.argk[i], parser.argv[i]);
+         switch (status)
+         {
+         case bRC_OK:
+            /* the parameter was handled by xenctx, proceed to the next */
+            continue;
+         case bRC_Error:
+            /* parsing returned error, raise it up */
+            return bRC_Error;
+         default:
+            break;
+         }
+
+         DMSG(ctx, DERROR, "Unknown parameter: %s\n", parser.argk[i]);
+         JMSG(ctx, pluginctx_jmsg_err_level(), "Unknown parameter: %s\n", parser.argk[i]);
+      }
+
+#if 0
+      /* count the numbers of user parameters if any */
+      // count = get_ini_count();
+
+      /* the first (zero) parameter is a plugin name, we should skip it */
+      argc = parser.argc - 1;
+      parargc = argc + count;
+      /* first parameters from plugin command saved during backup */
+      for (int i = 1; i < parser.argc; i++) {
+         param = new POOL_MEM(PM_FNAME);     // TODO: change to POOL_MEM
+         found = false;
+
+         int k;
+         /* check if parameter overloaded by restore parameter */
+         if ((k = check_ini_param(parser.argk[i])) != -1){
+            found = true;
+            DMSG1(ctx, DINFO, "parse_plugin_command: %s found in restore parameters\n", parser.argk[i]);
+            if (render_param(ctx, *param, ini.items[k].handler, parser.argk[i], ini.items[k].val) != bRC_OK){
+               delete(param);
+               return bRC_Error;
+            }
+            params.append(param);
+            parargc--;
+         }
+
+         /* check if param overloaded above */
+         if (!found){
+            if (parser.argv[i]){
+               Mmsg(*param, "%s=%s\n", parser.argk[i], parser.argv[i]);
+               params.append(param);
+            } else {
+               Mmsg(*param, "%s=1\n", parser.argk[i]);
+               params.append(param);
+            }
+         }
+         /* param is always ended with '\n' */
+         DMSG(ctx, DINFO, "Param: %s", param);
+
+      }
+      /* check what was missing in plugin command but get from ini file */
+      if (argc < parargc){
+         for (int k = 0; ini.items[k].name; k++){
+            if (ini.items[k].found && !check_plugin_param(ini.items[k].name, &params)){
+               param = new POOL_MEM(PM_FNAME);
+               DMSG1(ctx, DINFO, "parse_plugin_command: %s from restore parameters\n", ini.items[k].name);
+               if (render_param(ctx, *param, ini.items[k].handler, (char*)ini.items[k].name, ini.items[k].val) != bRC_OK){
+                  delete(param);
+                  return bRC_Error;
+               }
+               params.append(param);
+               /* param is always ended with '\n' */
+               DMSG(ctx, DINFO, "Param: %s", param);
+            }
+         }
+      }
+#endif
+      return bRC_OK;
+   }
+
+   /**
+    * @brief Parse a Restore Object saved during backup. It handle both plugin config and other restore objects.
+    *
+    * @param ctx Bacula Plugin context structure
+    * @param rop a restore object structure to parse
+    * @return bRC bRC_OK - on success
+    *             bRC_Error - on error
+    */
+   bRC PLUGINBCLASS::parse_plugin_restore_object(bpContext *ctx, restore_object_pkt *rop)
+   {
+      if (!rop){
+         return bRC_OK;    /* end of rop list */
+      }
+
+      DMSG2(ctx, DDEBUG, "parse_plugin_restore_object: %s %d\n", rop->object_name, rop->object_type);
+
+      pluginctx_switch_command(rop->plugin_name);
+
+      // first check plugin config
+      if (strcmp(rop->object_name, INI_RESTORE_OBJECT_NAME) == 0 && (rop->object_type == FT_PLUGIN_CONFIG || rop->object_type == FT_PLUGIN_CONFIG_FILLED)) {
+         /* we have a single config RO for every command */
+         DMSG(ctx, DINFO, "plugin config for: %s\n", rop->plugin_name);
+         bRC status = parse_plugin_config(ctx, rop);
+         if (status != bRC_OK) {
+            return bRC_Error;
+         }
+         return pluginctx_parse_plugin_config(ctx, rop);
+      }
+
+      // handle any other RO restore
+      bRC status = handle_plugin_restore_object(ctx, rop);
+      if (status != bRC_OK) {
+         return bRC_Error;
+      }
+
+      return pluginctx_handle_restore_object(ctx, rop);
+   }
+
+   /**
+    * @brief Handle Bacula Plugin I/O API
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param io Bacula Plugin API I/O structure for I/O operations
+    * @return bRC bRC_OK - when successful
+    *             bRC_Error - on any error
+    *             io->status, io->io_errno - correspond to a plugin io operation status
+    */
+   bRC PLUGINBCLASS::pluginIO(bpContext *ctx, struct io_pkt *io)
+   {
+      static int rw = 0;      // this variable handles single debug message
+
+      CHECK_JOB_CANCELLED;
+
+      /* assume no error from the very beginning */
+      io->status = 0;
+      io->io_errno = 0;
+      switch (io->func)
+      {
+      case IO_OPEN:
+         DMSG(ctx, D2, "IO_OPEN: (%s)\n", io->fname);
+         switch (mode)
+         {
+         case BackupFull:
+#if __cplusplus >= 201703L
+            [[fallthrough]];
+#endif
+         case BackupDiff:
+#if __cplusplus >= 201703L
+            [[fallthrough]];
+#endif
+         case BackupIncr:
+            return perform_backup_open(ctx, io);
+
+         case Restore:
+            // nodata = true;
+            return perform_restore_open(ctx, io);
+
+         default:
+            return bRC_Error;
+         }
+         break;
+
+      case IO_READ:
+         if (!rw) {
+            rw = 1;
+            DMSG2(ctx, D2, "IO_READ buf=%p len=%d\n", io->buf, io->count);
+         }
+         switch (mode)
+         {
+         case BackupFull:
+#if __cplusplus >= 201703L
+            [[fallthrough]];
+#endif
+         case BackupDiff:
+#if __cplusplus >= 201703L
+            [[fallthrough]];
+#endif
+         case BackupIncr:
+            return perform_read_data(ctx, io);
+
+         default:
+            return bRC_Error;
+         }
+         break;
+      case IO_WRITE:
+         if (!rw) {
+            rw = 1;
+            DMSG2(ctx, D2, "IO_WRITE buf=%p len=%d\n", io->buf, io->count);
+         }
+         switch (mode)
+         {
+         case Restore:
+            return perform_write_data(ctx, io);
+
+         default:
+            return bRC_Error;
+         }
+         break;
+
+      case IO_CLOSE:
+         DMSG0(ctx, D2, "IO_CLOSE\n");
+         rw = 0;
+         switch (mode)
+         {
+         case Restore:
+            return perform_restore_close(ctx, io);
+
+         case BackupFull:
+#if __cplusplus >= 201703L
+            [[fallthrough]];
+#endif
+         case BackupDiff:
+#if __cplusplus >= 201703L
+            [[fallthrough]];
+#endif
+         case BackupIncr:
+            return perform_backup_close(ctx, io);
+         default:
+            return bRC_Error;
+         }
+         break;
+
+      case IO_SEEK:
+         DMSG2(ctx, D2, "IO_SEEK off=%lld wh=%d\n", io->offset, io->whence);
+         switch (mode)
+         {
+         case Restore:
+            return perform_seek_write(ctx, io);
+         default:
+            return bRC_Error;
+         }
+         break;
+      }
+
+      return bRC_OK;
+   }
+
+   /**
+    * @brief Get all required information from backend to populate save_pkt for Bacula.
+    *    It handles a Restore Object (FT_PLUGIN_CONFIG) for every Full backup and
+    *    new Plugin Backup Command if setup in FileSet. It uses a help from
+    *    endBackupFile() handling the next FNAME command for the next file to
+    *    backup. The communication protocol requires some file attributes command
+    *    required it raise the error when insufficient parameters received from
+    *    backend. It assumes some parameters at save_pkt struct to be automatically
+    *    set like: sp->portable, sp->statp.st_blksize, sp->statp.st_blocks.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param sp Bacula Plugin API save packet structure
+    * @return bRC bRC_OK - when save_pkt prepared successfully and we have file to backup
+    *             bRC_Max - when no more files to backup
+    *             bRC_Error - in any error
+    */
+   bRC PLUGINBCLASS::startBackupFile(bpContext *ctx, struct save_pkt *sp)
+   {
+      CHECK_JOB_CANCELLED;
+
+      /* The first file in Full backup, is the Plugin Config */
+      if (mode == BackupFull && pluginconfigsent == false) {
+         ConfigFile ini;
+         ini.register_items(plugin_items_dump, sizeof(struct ini_items));
+         sp->restore_obj.object_name = (char *)INI_RESTORE_OBJECT_NAME;
+         sp->restore_obj.object_len = ini.serialize(robjbuf.handle());
+         sp->restore_obj.object = robjbuf.c_str();
+         sp->type = FT_PLUGIN_CONFIG;
+         DMSG2(ctx, DINFO, "Prepared RestoreObject/%s (%d) sent.\n", INI_RESTORE_OBJECT_NAME, FT_PLUGIN_CONFIG);
+         return bRC_OK;
+      }
+
+      bRC status = perform_start_backup_file(ctx, sp);
+
+      // DMSG3(ctx, DINFO, "TSDebug: %ld(at) %ld(mt) %ld(ct)\n",
+      //       sp->statp.st_atime, sp->statp.st_mtime, sp->statp.st_ctime);
+
+      return status;
+   }
+
+   /**
+    * @brief Check for a next file to backup or the end of the backup loop.
+    *    The next file to backup is indicated by a FNAME command from backend and
+    *    no more files to backup as EOD. It helps startBackupFile handling FNAME
+    *    for next file.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @return bRC bRC_OK - when no more files to backup
+    *             bRC_More - when Bacula should expect a next file
+    *             bRC_Error - in any error
+    */
+   bRC PLUGINBCLASS::endBackupFile(bpContext *ctx)
+   {
+      CHECK_JOB_CANCELLED;
+
+      /* When current file was the Plugin Config, so just ask for the next file */
+      if (mode == BackupFull && pluginconfigsent == false) {
+         pluginconfigsent = true;
+         return bRC_More;
+      }
+
+      bRC status = perform_end_backup_file(ctx);
+      if (status == bRC_More) {
+         DMSG1(ctx, DINFO, "Nextfile %s backup!\n", fname.c_str());
+      }
+
+      return status;
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx
+    * @param cmd
+    * @return bRC
+    */
+   bRC PLUGINBCLASS::startRestoreFile(bpContext *ctx, const char *cmd)
+   {
+      CHECK_JOB_CANCELLED;
+
+      return perform_start_restore_file(ctx, cmd);
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx
+    * @return bRC
+    */
+   bRC PLUGINBCLASS::endRestoreFile(bpContext *ctx)
+   {
+      CHECK_JOB_CANCELLED;
+
+      return perform_end_restore_file(ctx);
+   }
+
+   /**
+    * @brief Prepares a file to restore attributes based on data from restore_pkt.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param rp Bacula Plugin API restore packet structure
+    * @return bRC bRC_OK - when success
+    *             rp->create_status = CF_EXTRACT - the plugin will restore the file itself
+    *             rp->create_status = CF_SKIP - the plugin wants to skip restoration, i.e. the file already exist and Replace=n was set
+    *             rp->create_status = CF_CORE - the plugin wants Bacula to do the restore
+    *             bRC_Error, rp->create_status = CF_ERROR - in any error
+    */
+   bRC PLUGINBCLASS::createFile(bpContext *ctx, struct restore_pkt *rp)
+   {
+      CHECK_JOB_CANCELLED;
+
+      if (CORELOCALRESTORE && islocalpath(where)) {
+         DMSG0(ctx, DDEBUG, "createFile:Forwarding restore to Core\n");
+         rp->create_status = CF_CORE;
+         return bRC_OK;
+      }
+
+      return perform_restore_create_file(ctx, rp);
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx
+    * @param rp
+    * @return bRC
+    */
+   bRC PLUGINBCLASS::setFileAttributes(bpContext *ctx, struct restore_pkt *rp)
+   {
+      CHECK_JOB_CANCELLED;
+
+      return perform_restore_set_file_attributes(ctx, rp);
+   }
+
+   /**
+    * @brief Implements default pluginclass checkFile() callback.
+    *    When fname match plugin configured namespace then it return bRC_Seen by default
+    *    if no custom perform_backup_check_file() method is implemented by developer.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param fname file name to check
+    * @return bRC bRC_Seen or bRC_OK
+    */
+   bRC PLUGINBCLASS::checkFile(bpContext * ctx, char *fname)
+   {
+      CHECK_JOB_CANCELLED;
+
+      if (isourpluginfname(PLUGINPREFIX, fname)) {
+         return perform_backup_check_file(ctx, fname);
+      }
+
+      return bRC_OK;
+   }
+
+   /**
+    * @brief Handle ACL and XATTR data during backup or restore.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param xacl
+    * @return bRC bRC_OK when successfull, OK
+    *             bRC_Error on any error found
+    */
+   bRC PLUGINBCLASS::handleXACLdata(bpContext *ctx, struct xacl_pkt *xacl)
+   {
+      CHECK_JOB_CANCELLED;
+
+      // defaults
+      xacl->count = 0;
+      xacl->content = NULL;
+
+      switch (xacl->func)
+      {
+      case BACL_BACKUP:
+         return perform_acl_backup(ctx, xacl);
+      case BACL_RESTORE:
+         return perform_acl_restore(ctx, xacl);
+      case BXATTR_BACKUP:
+         return perform_xattr_backup(ctx, xacl);
+      case BXATTR_RESTORE:
+         return perform_xattr_restore(ctx, xacl);
+      default:
+         DMSG1(ctx, DERROR, "PLUGINBCLASS::handleXACLdata: unknown xacl function: %d\n", xacl->func);
+      }
+
+      return bRC_OK;
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param qp
+    * @return bRC bRC_OK when successfull, OK
+    *             bRC_Error on any error found
+    */
+   bRC PLUGINBCLASS::queryParameter(bpContext *ctx, struct query_pkt *qp)
+   {
+      // check if it is our Plugin command
+      if (!isourplugincommand(PLUGINPREFIX, qp->command) != 0){
+         // it is not our plugin prefix
+         return bRC_OK;
+      }
+
+      CHECK_JOB_CANCELLED;
+
+      if (mode != QueryParams) {
+         mode = QueryParams;
+      }
+
+      return perform_query_parameter(ctx, qp);
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param mp
+    * @return bRC bRC_OK when successfull, OK
+    *             bRC_Error on any error found
+    */
+   bRC PLUGINBCLASS::metadataRestore(bpContext *ctx, struct meta_pkt *mp)
+   {
+      CHECK_JOB_CANCELLED;
+
+      return perform_restore_metadata(ctx, mp);
+   }
+
+}  // namespace pluginlib
diff --git a/bacula/src/plugins/fd/pluginlib/pluginclass.h b/bacula/src/plugins/fd/pluginlib/pluginclass.h
new file mode 100644 (file)
index 0000000..5f16beb
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2023 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.
+ */
+/**
+ * @file pluginclass.h
+ * @author Radosław Korzeniewski (radoslaw@korzeniewski.net)
+ * @brief This is a Bacula File Daemon general plugin framework. The Class.
+ * @version 1.0.0
+ * @date 2021-04-08
+ *
+ * @copyright Copyright (c) 2021 All rights reserved. IP transferred to Bacula Systems according to agreement.
+ */
+
+#include "pluginlib.h"
+#include "lib/ini.h"
+#include "pluginlib/commctx.h"
+#include "pluginlib/smartalist.h"
+
+#define USE_CMD_PARSER
+#include "fd_common.h"
+
+#ifndef PLUGINLIB_PLUGINCLASS_H
+#define PLUGINLIB_PLUGINCLASS_H
+
+/// The list of restore options saved to the Plugin Config restore object.
+extern struct ini_items plugin_items_dump[];
+
+/// defines if plugin should handle local filesystem restore with Bacula Core functions
+/// `false` means plugin will handle local restore itself
+/// `true` means Bacula Core functions will handle local restore
+extern const bool CORELOCALRESTORE;
+
+namespace pluginlib
+{
+   /*
+    * This is a main plugin API class. It manages a plugin context.
+    *  All the public methods correspond to a public Bacula API calls, even if
+    *  a callback is not implemented.
+    */
+   class PLUGINBCLASS: public SMARTALLOC
+   {
+   public:
+      enum MODE
+      {
+         None = 0,
+         BackupFull,
+         BackupIncr,
+         BackupDiff,
+         EstimateFull,
+         EstimateIncr,
+         EstimateDiff,
+         Listing,
+         QueryParams,
+         Restore,
+      };
+      enum OBJECT
+      {
+         FileObject,
+         PluginObject,
+         RestoreObject,
+      };
+
+      virtual bRC getPluginValue(bpContext *ctx, pVariable var, void *value) { return bRC_OK; }
+      virtual bRC setPluginValue(bpContext *ctx, pVariable var, void *value) { return bRC_OK; }
+      virtual bRC handlePluginEvent(bpContext *ctx, bEvent *event, void *value);
+      virtual bRC startBackupFile(bpContext *ctx, struct save_pkt *sp);
+      virtual bRC endBackupFile(bpContext *ctx);
+      virtual bRC startRestoreFile(bpContext *ctx, const char *cmd);
+      virtual bRC endRestoreFile(bpContext *ctx);
+      virtual bRC pluginIO(bpContext *ctx, struct io_pkt *io);
+      virtual bRC createFile(bpContext *ctx, struct restore_pkt *rp);
+      virtual bRC setFileAttributes(bpContext *ctx, struct restore_pkt *rp);
+      virtual bRC checkFile(bpContext *ctx, char *fname);
+      virtual bRC handleXACLdata(bpContext *ctx, struct xacl_pkt *xacl);
+      virtual bRC queryParameter(bpContext *ctx, struct query_pkt *qp);
+      virtual bRC metadataRestore(bpContext *ctx, struct meta_pkt *mp);
+
+      void setup_plugin(bpContext *ctx);
+
+      PLUGINBCLASS(bpContext *bpctx) :
+         mode(None),
+         JobId(0),
+         JobName(NULL),
+         since(0),
+         where(NULL),
+         regexwhere(NULL),
+         replace(0),
+         pluginconfigsent(false),
+         object(FileObject),
+         fname(PM_FNAME),
+         lname(PM_FNAME),
+         robjbuf(PM_MESSAGE),
+         parser(),
+         execpath(PM_FNAME),
+         workingpath(PM_FNAME),
+         job_cancelled(false)
+      {}
+#if __cplusplus > 201103L
+      PLUGINBCLASS() = delete;
+      PLUGINBCLASS(PLUGINBCLASS&) = delete;
+      PLUGINBCLASS(PLUGINBCLASS&&) = delete;
+#endif
+      virtual ~PLUGINBCLASS() {}
+
+   protected:
+      MODE mode;                       /// Plugin mode of operation
+      int JobId;                       /// Job ID
+      char *JobName;                   /// Job name
+      time_t since;                    /// Job since parameter
+      char *where;                     /// the Where variable for restore job if set by user
+      char *regexwhere;                /// the RegexWhere variable for restore job if set by user
+      char replace;                    /// the replace variable for restore job
+      bool pluginconfigsent;           /// set when Plugin Config was sent during Full backup
+      OBJECT object;                   /// the object type to handdle
+      POOL_MEM fname;                  /// current file name to backup (grabbed from backend)
+      POOL_MEM lname;                  /// current LSTAT data if any
+      POOL_MEM robjbuf;                /// the buffer for restore object data
+      cmd_parser parser;               /// Plugin command parser
+      POOL_MEM execpath;               /// ready to use path where bacula binaries are located
+      POOL_MEM workingpath;            /// ready to use path for bacula working directory
+      bool job_cancelled;              /// it signal the metaplugin that job was cancelled
+
+      virtual bRC parse_plugin_config(bpContext *ctx, restore_object_pkt *rop) { return bRC_OK; }
+      virtual bRC parse_plugin_command(bpContext *ctx, const char *command);
+
+      virtual bRC parse_plugin_restore_object(bpContext *ctx, restore_object_pkt *rop);
+      virtual bRC handle_plugin_restore_object(bpContext *ctx, restore_object_pkt *rop) { return bRC_OK; }
+
+      virtual bRC prepare_estimate(bpContext *ctx, char *command) { return bRC_OK; }
+      virtual bRC prepare_backup(bpContext *ctx, char *command) { return bRC_OK; }
+      virtual bRC prepare_restore(bpContext *ctx, char *command) { return bRC_OK; }
+      virtual bRC prepare_command(bpContext *ctx, char *command) { return bRC_OK; }
+
+      virtual bRC perform_backup_open(bpContext *ctx, struct io_pkt *io) { return bRC_OK; }
+      virtual bRC perform_restore_open(bpContext *ctx, struct io_pkt *io) { return bRC_OK; }
+      virtual bRC perform_read_data(bpContext *ctx, struct io_pkt *io) { return bRC_OK; }
+      virtual bRC perform_write_data(bpContext *ctx, struct io_pkt *io) { return bRC_OK; }
+      virtual bRC perform_seek_write(bpContext *ctx, struct io_pkt *io) { return bRC_OK; }
+      virtual bRC perform_restore_close(bpContext *ctx, struct io_pkt *io) { return bRC_OK; }
+      virtual bRC perform_backup_close(bpContext *ctx, struct io_pkt *io) { return bRC_OK; }
+      virtual bRC perform_restore_create_file(bpContext *ctx, struct restore_pkt *rp) { return bRC_OK; }
+      virtual bRC perform_restore_set_file_attributes(bpContext *ctx, struct restore_pkt *rp) { return bRC_OK; }
+
+      virtual bRC perform_backup_check_file(bpContext *ctx, char *fname) { return bRC_Seen; }
+      virtual bRC perform_acl_backup(bpContext *ctx, struct xacl_pkt *xacl) { return bRC_OK; }
+      virtual bRC perform_acl_restore(bpContext *ctx, struct xacl_pkt *xacl) { return bRC_OK; }
+      virtual bRC perform_xattr_backup(bpContext *ctx, struct xacl_pkt *xacl) { return bRC_OK; }
+      virtual bRC perform_xattr_restore(bpContext *ctx, struct xacl_pkt *xacl) { return bRC_OK; }
+
+      virtual bRC perform_query_parameter(bpContext *ctx, struct query_pkt *qp) { return bRC_OK; }
+      virtual bRC perform_restore_metadata(bpContext *ctx, struct meta_pkt *mp) { return bRC_OK; }
+
+      virtual bRC perform_jobstart(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_backupjobstart(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_restorejobstart(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_jobend(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_backupjobend(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_restorejobend(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_joblevel(bpContext *ctx, char lvl) { return bRC_OK; }
+      virtual bRC perform_jobsince(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_jobendfileset(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_start_backup_file(bpContext *ctx, struct save_pkt *sp) { return bRC_Max; }
+      virtual bRC perform_start_restore_file(bpContext *ctx, const char *cmd) { return bRC_OK; }
+      virtual bRC perform_end_backup_file(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_end_restore_file(bpContext *ctx) { return bRC_OK; }
+      virtual bRC perform_cancel_command(bpContext *ctx) { return bRC_OK; }
+
+      // virtual int check_ini_param(char *param);
+
+      virtual void pluginctx_switch_command(const char *command) {}
+      virtual void pluginctx_clear_abort_on_error() {}
+      virtual void pluginctx_set_abort_on_error() {}
+      virtual bRC pluginctx_parse_parameter(bpContext *ctx, const char *argk, const char *argv) { return bRC_Error; }
+      virtual bRC pluginctx_parse_parameter(bpContext *ctx, ini_items &item) { return bRC_Error; }
+      virtual bool pluginctx_is_abort_on_error() { return false; }
+      virtual int pluginctx_jmsg_err_level() { return -1; }
+      virtual bRC pluginctx_parse_plugin_config(bpContext *ctx, restore_object_pkt *rop) { return bRC_Error; }
+      virtual bRC pluginctx_handle_restore_object(bpContext *ctx, restore_object_pkt *rop) { return bRC_Error; }
+   };
+
+   /**
+    * @brief A final template class instantinated with custom Plugin Context.
+    *
+    * @tparam CTX a custom Plugin Context to use
+    */
+   template<class CTX>
+   class PLUGINCLASS : public PLUGINBCLASS
+   {
+   public:
+      PLUGINCLASS(bpContext *bpctx) : PLUGINBCLASS(bpctx) {}
+#if __cplusplus > 201103L
+      PLUGINCLASS() = delete;
+      PLUGINCLASS(PLUGINCLASS&) = delete;
+      PLUGINCLASS(PLUGINCLASS&&) = delete;
+#endif
+      virtual ~PLUGINCLASS() {}
+
+   protected:
+      COMMCTX<CTX> pluginctx;          /// the current plugin execution context
+
+      virtual void pluginctx_switch_command(const char *command) { pluginctx.switch_command(command); }
+      virtual void pluginctx_clear_abort_on_error() { pluginctx->clear_abort_on_error(); }
+      virtual void pluginctx_set_abort_on_error() { pluginctx->set_abort_on_error(); }
+      virtual bRC pluginctx_parse_parameter(bpContext *ctx, const char *argk, const char *argv) { return pluginctx->parse_parameter(ctx, argk, argv); }
+      virtual bRC pluginctx_parse_parameter(bpContext *ctx, ini_items &item) { return pluginctx->parse_parameter(ctx, item); }
+      virtual bool pluginctx_is_abort_on_error() { return pluginctx->is_abort_on_error(); }
+      virtual int pluginctx_jmsg_err_level() { return pluginctx->jmsg_err_level(); }
+      virtual bRC pluginctx_parse_plugin_config(bpContext *ctx, restore_object_pkt *rop) { return pluginctx->parse_plugin_config(ctx, rop); }
+      virtual bRC pluginctx_handle_restore_object(bpContext *ctx, restore_object_pkt *rop) { return pluginctx->handle_restore_object(ctx, rop); }
+   };
+}  // namespace pluginlib
+
+#endif   // PLUGINLIB_PLUGINCLASS_H
diff --git a/bacula/src/plugins/fd/pluginlib/pluginctx.cpp b/bacula/src/plugins/fd/pluginlib/pluginctx.cpp
new file mode 100644 (file)
index 0000000..00299e2
--- /dev/null
@@ -0,0 +1,2697 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2023 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.
+ */
+/**
+ * @file pluginctx.cpp
+ * @author Radosław Korzeniewski (radoslaw@korzeniewski.net)
+ * @brief This is a Bacula File Daemon general plugin framework. The Ccontext.
+ * @version 1.0.0
+ * @date 2021-04-08
+ *
+ * @copyright Copyright (c) 2021 All rights reserved. IP transferred to Bacula Systems according to agreement.
+ */
+
+#include "pluginctx.h"
+#include <sys/stat.h>
+#include <signal.h>
+#include <sys/select.h>
+
+/*
+ * libbac uses its own sscanf implementation which is not compatible with
+ * libc implementation, unfortunately.
+ * use bsscanf for Bacula sscanf flavor
+ */
+#ifdef sscanf
+#undef sscanf
+#endif
+
+namespace pluginlib
+{
+   /**
+    * @brief Check if a parameter (param) exist in ConfigFile variables set by user.
+    *          The checking ignore case of the parameter.
+    *
+    * @param param a parameter to search in ini parameter keys
+    * @return int -1 - when a parameter param is not found in ini keys
+    *             <n> - whan a parameter param is found and <n> is an index in ini->items table
+    */
+   int PLUGINCTX::check_ini_param(char *param)
+   {
+      if (ini.items){
+         for (int k = 0; ini.items[k].name; k++){
+            if (ini.items[k].found && strcasecmp(param, ini.items[k].name) == 0){
+               return k;
+            }
+         }
+      }
+
+      return -1;
+   }
+
+   bRC PLUGINCTX::parse_plugin_config(bpContext *ctx, restore_object_pkt *rop)
+   {
+      ini.clear_items();
+      if (!ini.dump_string(rop->object, rop->object_len)) {
+         DMSG0(ctx, DERROR, "ini->dump_string failed\n");
+         JMSG0(ctx, M_FATAL, "Unable to parse user set restore configuration.\n");
+         return bRC_Error;
+      }
+
+      ini.register_items(plugin_items_dump, sizeof(struct ini_items));
+      if (!ini.parse(ini.out_fname)) {
+         DMSG0(ctx, DERROR, "ini->parse failed\n");
+         JMSG0(ctx, M_FATAL, "Unable to parse user set restore configuration.\n");
+         return bRC_Error;
+      }
+
+      for (int i = 0; ini.items[i].name; i++) {
+         if (ini.items[i].found) {
+            if (ini.items[i].handler == ini_store_str) {
+               DMSG2(ctx, DINFO, "INI: %s = %s\n", ini.items[i].name, ini.items[i].val.strval);
+            } else
+            if (ini.items[i].handler == ini_store_int64) {
+               DMSG2(ctx, DINFO, "INI: %s = %lld\n", ini.items[i].name, ini.items[i].val.int64val);
+            } else
+            if (ini.items[i].handler == ini_store_bool) {
+               DMSG2(ctx, DINFO, "INI: %s = %s\n", ini.items[i].name, ini.items[i].val.boolval ? "True" : "False");
+            } else {
+               DMSG1(ctx, DERROR, "INI: unsupported parameter handler for: %s\n", ini.items[i].name);
+               JMSG1(ctx, M_FATAL, "INI: unsupported parameter handler for: %s\n", ini.items[i].name);
+               return bRC_Error;
+            }
+         }
+      }
+
+      return bRC_OK;
+   }
+
+
+
+
+#if 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+   /**
+    * @brief Search if parameter (param) is on parameter list prepared for backend.
+    *          The checking ignore case of the parameter.
+    *
+    * @param param the parameter which we are looking for
+    * @param params the list of parameters to search
+    * @return true when the parameter param is found in list
+    * @return false when we can't find the param on list
+    */
+   bool PLUGINCLASS::check_plugin_param(const char *param, alist *params)
+   {
+      POOLMEM *par;
+      char *equal;
+      bool found = false;
+
+      foreach_alist(par, params){
+         equal = strchr(par, '=');
+         if (equal){
+            /* temporary terminate the par at parameter name */
+            *equal = '\0';
+            if (strcasecmp(par, param) == 0){
+               found = true;
+            }
+            /* restore parameter equal sign */
+            *equal = '=';
+         } else {
+            if (strcasecmp(par, param) == 0){
+               found = true;
+            }
+         }
+      }
+
+      return found;
+   }
+
+   /**
+    * @brief Counts the number of ini->items available as it is a NULL terminated array.
+    *
+    * @return int the number of ini->items
+    */
+   int PLUGINCLASS::get_ini_count()
+   {
+      int count = 0;
+
+      if (ini.items){
+         for (int k = 0; ini.items[k].name; k++){
+            if (ini.items[k].found){
+               count++;
+            }
+         }
+      }
+
+      return count;
+   }
+
+   /**
+    * @brief Parsing a plugin command.
+    *
+    * @param ctx bpContext - Bacula Plugin context structure
+    * @param command plugin command string to parse
+    * @param params output parsed params list
+    * @return bRC bRC_OK - on success, bRC_Error - on error
+    */
+   bRC PLUGINCLASS::parse_plugin_command(bpContext *ctx, const char *command, smart_alist<POOL_MEM> &params)
+   {
+      bool found;
+      int count;
+      int parargc, argc;
+      POOL_MEM *param;
+
+      DMSG(ctx, DINFO, "Parse command: %s\n", command);
+      if (parser.parse_cmd(command) != bRC_OK)
+      {
+         DMSG0(ctx, DERROR, "Unable to parse Plugin command line.\n");
+         JMSG0(ctx, M_FATAL, "Unable to parse Plugin command line.\n");
+         return bRC_Error;
+      }
+
+      /* count the numbers of user parameters if any */
+      count = get_ini_count();
+
+      /* the first (zero) parameter is a plugin name, we should skip it */
+      argc = parser.argc - 1;
+      parargc = argc + count;
+      /* first parameters from plugin command saved during backup */
+      for (int i = 1; i < parser.argc; i++) {
+         param = new POOL_MEM(PM_FNAME);     // TODO: change to POOL_MEM
+         found = false;
+
+         int k;
+         /* check if parameter overloaded by restore parameter */
+         if ((k = check_ini_param(parser.argk[i])) != -1){
+            found = true;
+            DMSG1(ctx, DINFO, "parse_plugin_command: %s found in restore parameters\n", parser.argk[i]);
+            if (render_param(ctx, *param, ini.items[k].handler, parser.argk[i], ini.items[k].val) != bRC_OK){
+               delete(param);
+               return bRC_Error;
+            }
+            params.append(param);
+            parargc--;
+         }
+
+         /* check if param overloaded above */
+         if (!found){
+            if (parser.argv[i]){
+               Mmsg(*param, "%s=%s\n", parser.argk[i], parser.argv[i]);
+               params.append(param);
+            } else {
+               Mmsg(*param, "%s=1\n", parser.argk[i]);
+               params.append(param);
+            }
+         }
+         /* param is always ended with '\n' */
+         DMSG(ctx, DINFO, "Param: %s", param);
+
+         /* scan for abort_on_error parameter */
+         if (strcasecmp(parser.argk[i], "abort_on_error") == 0){
+            /* found, so check the value if provided, I only check the first char */
+            if (parser.argv[i] && *parser.argv[i] == '0'){
+               backend.ctx->clear_abort_on_error();
+            } else {
+               backend.ctx->set_abort_on_error();
+            }
+            DMSG1(ctx, DINFO, "abort_on_error found: %s\n", backend.ctx->is_abort_on_error() ? "True" : "False");
+         }
+         /* scan for listing parameter, so the estimate job should be executed as a Listing procedure */
+         if (strcasecmp(parser.argk[i], "listing") == 0){
+            /* found, so check the value if provided */
+            if (parser.argv[i]){
+               listing = Listing;
+               DMSG0(ctx, DINFO, "listing procedure param found\n");
+            }
+         }
+         /* scan for query parameter, so the estimate job should be executed as a QueryParam procedure */
+         if (strcasecmp(parser.argk[i], "query") == 0){
+            /* found, so check the value if provided */
+            if (parser.argv[i]){
+               listing = Query;
+               DMSG0(ctx, DINFO, "query procedure param found\n");
+            }
+         }
+      }
+      /* check what was missing in plugin command but get from ini file */
+      if (argc < parargc){
+         for (int k = 0; ini.items[k].name; k++){
+            if (ini.items[k].found && !check_plugin_param(ini.items[k].name, &params)){
+               param = new POOL_MEM(PM_FNAME);
+               DMSG1(ctx, DINFO, "parse_plugin_command: %s from restore parameters\n", ini.items[k].name);
+               if (render_param(ctx, *param, ini.items[k].handler, (char*)ini.items[k].name, ini.items[k].val) != bRC_OK){
+                  delete(param);
+                  return bRC_Error;
+               }
+               params.append(param);
+               /* param is always ended with '\n' */
+               DMSG(ctx, DINFO, "Param: %s", param);
+            }
+         }
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Parse a Restore Object saved during backup and modified by user during restore.
+   *    Every RO received will generate a dedicated backend context which is used
+   *    by bEventRestoreCommand to handle backend parameters for restore.
+   *
+   * in:
+   *    bpContext - Bacula Plugin context structure
+   *    rop - a restore object structure to parse
+   * out:
+   *    bRC_OK - on success
+   *    bRC_Error - on error
+   */
+   bRC PLUGINCLASS::handle_plugin_restoreobj(bpContext *ctx, restore_object_pkt *rop)
+   {
+      if (!rop){
+         return bRC_OK;    /* end of rop list */
+      }
+
+      DMSG2(ctx, DDEBUG, "handle_plugin_restoreobj: %s %d\n", rop->object_name, rop->object_type);
+
+      // if (strcmp(rop->object_name, INI_RESTORE_OBJECT_NAME) == 0) {
+      if (strcmp(rop->object_name, INI_RESTORE_OBJECT_NAME) == 0 && (rop->object_type == FT_PLUGIN_CONFIG || rop->object_type == FT_PLUGIN_CONFIG_FILLED)) {
+
+         DMSG(ctx, DINFO, "INIcmd: %s\n", rop->plugin_name);
+
+         ini.clear_items();
+         if (!ini.dump_string(rop->object, rop->object_len))
+         {
+            DMSG0(ctx, DERROR, "ini->dump_string failed\n");
+            JMSG0(ctx, M_FATAL, "Unable to parse user set restore configuration.\n");
+            return bRC_Error;
+         }
+
+         ini.register_items(plugin_items_dump, sizeof(struct ini_items));
+         if (!ini.parse(ini.out_fname))
+         {
+            DMSG0(ctx, DERROR, "ini->parse failed\n");
+            JMSG0(ctx, M_FATAL, "Unable to parse user set restore configuration.\n");
+            return bRC_Error;
+         }
+
+         for (int i = 0; ini.items[i].name; i++) {
+            if (ini.items[i].found){
+               if (ini.items[i].handler == ini_store_str){
+                  DMSG2(ctx, DINFO, "INI: %s = %s\n", ini.items[i].name, ini.items[i].val.strval);
+               } else
+               if (ini.items[i].handler == ini_store_int64){
+                  DMSG2(ctx, DINFO, "INI: %s = %lld\n", ini.items[i].name, ini.items[i].val.int64val);
+               } else
+               if (ini.items[i].handler == ini_store_bool){
+                  DMSG2(ctx, DINFO, "INI: %s = %s\n", ini.items[i].name, ini.items[i].val.boolval ? "True" : "False");
+               } else {
+                  DMSG1(ctx, DERROR, "INI: unsupported parameter handler for: %s\n", ini.items[i].name);
+                  JMSG1(ctx, M_FATAL, "INI: unsupported parameter handler for: %s\n", ini.items[i].name);
+                  return bRC_Error;
+               }
+            }
+         }
+
+         return bRC_OK;
+      }
+
+      // handle any other RO restore
+      restore_object_class *ropclass = new restore_object_class;
+      ropclass->sent = false;
+      pm_strcpy(ropclass->plugin_name, rop->plugin_name);
+      pm_strcpy(ropclass->object_name, rop->object_name);
+      ropclass->length = rop->object_len;
+      pm_memcpy(ropclass->data, rop->object, rop->object_len);
+      restoreobject_list.append(ropclass);
+      DMSG2(ctx, DINFO, "ROclass saved for later: %s %d\n", ropclass->object_name.c_str(), ropclass->length);
+
+      return bRC_OK;
+   }
+
+   /*
+   * Run external backend script/application using BACKEND_CMD compile variable.
+   *    It will run the backend in current backend context (backendctx) and should
+   *    be called when a new backend is really required only.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when backend spawned successfully
+   *    bRC_Error - when Plugin cannot run backend
+   */
+   bRC PLUGINCLASS::run_backend(bpContext *ctx)
+   {
+      BPIPE *bp;
+
+      if (access(backend_cmd.c_str(), X_OK) < 0){
+         berrno be;
+         DMSG2(ctx, DERROR, "Unable to access backend: %s Err=%s\n", backend_cmd.c_str(), be.bstrerror());
+         JMSG2(ctx, M_FATAL, "Unable to access backend: %s Err=%s\n", backend_cmd.c_str(), be.bstrerror());
+         return bRC_Error;
+      }
+      DMSG(ctx, DINFO, "Executing: %s\n", backend_cmd.c_str());
+      bp = open_bpipe(backend_cmd.c_str(), 0, "rwe");
+      if (bp == NULL){
+         berrno be;
+         DMSG(ctx, DERROR, "Unable to run backend. Err=%s\n", be.bstrerror());
+         JMSG(ctx, M_FATAL, "Unable to run backend. Err=%s\n", be.bstrerror());
+         return bRC_Error;
+      }
+      /* setup communication channel */
+      backend.ctx->set_bpipe(bp);
+      DMSG(ctx, DINFO, "Backend executed at PID=%i\n", bp->worker_pid);
+      return bRC_OK;
+   }
+
+   bRC backendctx_finish_func(PTCOMM *ptcomm, void *cp)
+   {
+      bpContext * ctx = (bpContext*)cp;
+      bRC status = bRC_OK;
+      POOL_MEM cmd(PM_FNAME);
+      pm_strcpy(cmd, "FINISH\n");
+
+      if (!ptcomm->write_command(ctx, cmd.addr())){
+         status = bRC_Error;
+      }
+      if (!ptcomm->read_ack(ctx)){
+         status = bRC_Error;
+      }
+
+      return status;
+   }
+
+   /*
+   * Sends a "FINISH" command to all executed backends indicating the end of
+   * "Restore loop".
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when operation was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::signal_finish_all_backends(bpContext *ctx)
+   {
+      return backend.foreach_command_status(backendctx_finish_func, ctx);
+   }
+
+   /*
+   * Send end job command to backend.
+   *    It terminates the backend when command sending was unsuccessful, as it is
+   *    the very last procedure in protocol.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    ptcomm - backend context
+   * out:
+   *    bRC_OK - when send command was successful
+   *    bRC_Error - on any error
+   */
+   bRC send_endjob(bpContext *ctx, PTCOMM *ptcomm)
+   {
+      bRC status = bRC_OK;
+      POOL_MEM cmd(PM_FNAME);
+      pm_strcpy(cmd, "END\n");
+
+      if (!ptcomm->write_command(ctx, cmd.c_str())){
+         /* error */
+         status = bRC_Error;
+      } else {
+         if (!ptcomm->read_ack(ctx)){
+            DMSG0(ctx, DERROR, "Wrong backend response to JobEnd command.\n");
+            JMSG0(ctx, ptcomm->jmsg_err_level(), "Wrong backend response to JobEnd command.\n");
+            status = bRC_Error;
+         }
+         ptcomm->signal_term(ctx);
+      }
+      return status;
+   }
+
+   /*
+   * Terminates the current backend pointed by ptcomm context.
+   *    The termination sequence consist of "End Job" protocol procedure and real
+   *    backend process termination including communication channel close.
+   *    When we'll get an error during "End Job" procedure then we inform the user
+   *    and terminate the backend as usual without unnecessary formalities. :)
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when backend termination was successful, i.e. no error in
+   *             "End Job" procedure
+   *    bRC_Error - when backend termination encountered an error.
+   */
+   bRC backendctx_jobend_func(PTCOMM *ptcomm, void *cp)
+   {
+      bpContext *ctx = (bpContext *)cp;
+      bRC status = bRC_OK;
+
+      if (send_endjob(ctx, ptcomm) != bRC_OK){
+         /* error in end job */
+         DMSG0(ctx, DERROR, "Error in EndJob.\n");
+         status = bRC_Error;
+      }
+      int pid = ptcomm->get_backend_pid();
+      DMSG(ctx, DINFO, "Terminate backend at PID=%d\n", pid)
+      ptcomm->terminate(ctx);
+
+      return status;
+   }
+
+   /*
+   * Terminate all executed backends.
+   *    Check PLUGINCLASS::terminate_current_backend for more info.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when all backends termination was successful
+   *    bRC_Error - when any backend termination encountered an error
+   */
+   bRC PLUGINCLASS::terminate_all_backends(bpContext *ctx)
+   {
+      return backend.foreach_command_status(backendctx_jobend_func, ctx);
+   }
+
+   /**
+    * @brief Callback used for sending a `cancel event` to the selected backend
+    *
+    * @param ptcomm the backend communication object
+    * @param cp a bpContext - for Bacula debug and jobinfo messages
+    * @return bRC bRC_OK when success
+    */
+   bRC backendctx_cancel_func(PTCOMM *ptcomm, void *cp)
+   {
+      bpContext * ctx = (bpContext*)cp;
+
+      // cancel procedure
+      // 1. get backend pid
+      // 2. send SIGUSR1 to backend pid
+
+      pid_t pid = ptcomm->get_backend_pid();
+      DMSG(ctx, DINFO, "Inform backend about Cancel at PID=%d ...\n", pid)
+      kill(pid, SIGUSR1);
+
+      return bRC_OK;
+   }
+
+   /**
+    * @brief Send `cancel event` to every backend and terminate it.
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @return bRC bRC_OK when success, bRC_Error if not
+    */
+   bRC PLUGINCLASS::cancel_all_backends(bpContext *ctx)
+   {
+      PLUGINCLASS *pctx = (PLUGINCLASS *)ctx->pContext;
+      // the cancel procedure: for all backends execute cancel func
+      return pctx->backend.foreach_command_status(backendctx_cancel_func, ctx);
+   }
+
+   /*
+   * Send a "Job Info" protocol procedure parameters.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    type - a char compliant with the protocol indicating what jobtype we run
+   * out:
+   *    bRC_OK - when send job info was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::send_jobinfo(bpContext *ctx, char type)
+   {
+      int32_t rc;
+      POOL_MEM cmd;
+      char lvl;
+
+      /* we will be sending Job Info data */
+      pm_strcpy(cmd, "Job\n");
+      rc = backend.ctx->write_command(ctx, cmd);
+      if (rc < 0){
+         /* error */
+         return bRC_Error;
+      }
+      /* required parameters */
+      Mmsg(cmd, "Name=%s\n", JobName);
+      rc = backend.ctx->write_command(ctx, cmd);
+      if (rc < 0){
+         /* error */
+         return bRC_Error;
+      }
+      Mmsg(cmd, "JobID=%i\n", JobId);
+      rc = backend.ctx->write_command(ctx, cmd);
+      if (rc < 0){
+         /* error */
+         return bRC_Error;
+      }
+      Mmsg(cmd, "Type=%c\n", type);
+      rc = backend.ctx->write_command(ctx, cmd);
+      if (rc < 0){
+         /* error */
+         return bRC_Error;
+      }
+      /* optional parameters */
+      if (mode != RESTORE){
+         switch (mode){
+            case BACKUP_FULL:
+               lvl = 'F';
+               break;
+            case BACKUP_DIFF:
+               lvl = 'D';
+               break;
+            case BACKUP_INCR:
+               lvl = 'I';
+               break;
+            default:
+               lvl = 0;
+         }
+         if (lvl){
+            Mmsg(cmd, "Level=%c\n", lvl);
+            rc = backend.ctx->write_command(ctx, cmd);
+            if (rc < 0){
+               /* error */
+            return bRC_Error;
+            }
+         }
+      }
+      if (since){
+         Mmsg(cmd, "Since=%ld\n", since);
+         rc = backend.ctx->write_command(ctx, cmd);
+         if (rc < 0){
+            /* error */
+            return bRC_Error;
+         }
+      }
+      if (where){
+         Mmsg(cmd, "Where=%s\n", where);
+         rc = backend.ctx->write_command(ctx, cmd);
+         if (rc < 0){
+            /* error */
+            return bRC_Error;
+         }
+      }
+      if (regexwhere){
+         Mmsg(cmd, "RegexWhere=%s\n", regexwhere);
+         rc = backend.ctx->write_command(ctx, cmd);
+         if (rc < 0){
+            /* error */
+            return bRC_Error;
+         }
+      }
+      if (replace){
+         Mmsg(cmd, "Replace=%c\n", replace);
+         rc = backend.ctx->write_command(ctx, cmd);
+         if (rc < 0){
+            /* error */
+            return bRC_Error;
+         }
+      }
+
+      if (CUSTOMNAMESPACE){
+         Mmsg(cmd, "Namespace=%s\n", PLUGINNAMESPACE);
+         rc = backend.ctx->write_command(ctx, cmd);
+         if (rc < 0){
+            /* error */
+            return bRC_Error;
+         }
+      }
+
+      if (CUSTOMPREVJOBNAME && prevjobname){
+         Mmsg(cmd, "PrevJobName=%s\n", prevjobname);
+         rc = backend.ctx->write_command(ctx, cmd);
+         if (rc < 0){
+            /* error */
+            return bRC_Error;
+         }
+      }
+
+      backend.ctx->signal_eod(ctx);
+
+      if (!backend.ctx->read_ack(ctx)){
+         DMSG0(ctx, DERROR, "Wrong backend response to Job command.\n");
+         JMSG0(ctx, backend.ctx->jmsg_err_level(), "Wrong backend response to Job command.\n");
+         return bRC_Error;
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Send a "Plugin Parameters" protocol procedure data.
+   *    It parse plugin command and ini parameters before sending it to backend.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    command - a Plugin command for a job
+   * out:
+   *    bRC_OK - when send parameters was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::send_parameters(bpContext *ctx, char *command)
+   {
+      int32_t rc;
+      bRC status = bRC_OK;
+      POOL_MEM cmd(PM_FNAME);
+      // alist params(16, not_owned_by_alist);
+      smart_alist<POOL_MEM> params;
+      POOL_MEM *param;
+      bool found;
+
+   #ifdef DEVELOPER
+      static const char *regress_valid_params[] =
+      {
+         // add special test_backend commands to handle regression tests
+         "regress_error_plugin_params",
+         "regress_error_start_job",
+         "regress_error_backup_no_files",
+         "regress_error_backup_stderr",
+         "regress_error_estimate_stderr",
+         "regress_error_listing_stderr",
+         "regress_error_restore_stderr",
+         "regress_backup_plugin_objects",
+         "regress_backup_other_file",
+         "regress_error_backup_abort",
+         "regress_metadata_support",
+         "regress_standard_error_backup",
+         "regress_cancel_backup",
+         "regress_cancel_restore",
+         NULL,
+      };
+   #endif
+
+      /* parse and prepare final backend plugin params */
+      status = parse_plugin_command(ctx, command, params);
+      if (status != bRC_OK){
+         /* error */
+         return status;
+      }
+
+      /* send backend info that parameters are coming */
+      pm_strcpy(cmd, "Params\n");
+      rc = backend.ctx->write_command(ctx, cmd);
+      if (rc < 0){
+         /* error */
+         return bRC_Error;
+      }
+      /* send all prepared parameters */
+      foreach_alist(param, &params){
+         // check valid parameter list
+         found = false;
+         for (int a = 0; valid_params[a] != NULL; a++ )
+         {
+            DMSG3(ctx, DVDEBUG, "=> '%s' vs '%s' [%d]\n", param, valid_params[a], strlen(valid_params[a]));
+            if (strncasecmp(param->c_str(), valid_params[a], strlen(valid_params[a])) == 0){
+               found = true;
+               break;
+            }
+         }
+
+   #ifdef DEVELOPER
+         if (!found){
+            // now handle regression tests commands
+            for (int a = 0; regress_valid_params[a] != NULL; a++ ){
+               DMSG3(ctx, DVDEBUG, "regress=> '%s' vs '%s' [%d]\n", param, regress_valid_params[a], strlen(regress_valid_params[a]));
+               if (strncasecmp(param->c_str(), regress_valid_params[a], strlen(regress_valid_params[a])) == 0){
+                  found = true;
+                  break;
+               }
+            }
+         }
+   #endif
+
+         // signal error if required
+         if (!found) {
+            pm_strcpy(cmd, param->c_str());
+            strip_trailing_junk(cmd.c_str());
+            DMSG1(ctx, DERROR, "Unknown parameter %s in Plugin command.\n", cmd.c_str());
+            JMSG1(ctx, M_ERROR, "Unknown parameter %s in Plugin command.\n", cmd.c_str());
+         }
+
+         rc = backend.ctx->write_command(ctx, *param);
+         if (rc < 0) {
+            /* error */
+            return bRC_Error;
+         }
+      }
+
+      // now send accurate parameter if requested and available
+      if (ACCURATEPLUGINPARAMETER && accurate_mode) {
+         pm_strcpy(cmd, "Accurate=1\n");
+         rc = backend.ctx->write_command(ctx, cmd);
+         if (rc < 0) {
+            /* error */
+            return bRC_Error;
+         }
+      }
+
+      // signal end of parameters block
+      backend.ctx->signal_eod(ctx);
+      /* ack Params command */
+      if (!backend.ctx->read_ack(ctx)){
+         DMSG0(ctx, DERROR, "Wrong backend response to Params command.\n");
+         JMSG0(ctx, backend.ctx->jmsg_err_level(), "Wrong backend response to Params command.\n");
+         return bRC_Error;
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Send start job command pointed by command variable.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    command - the command string to send
+   * out:
+   *    bRC_OK - when send command was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::send_startjob(bpContext *ctx, const char *command)
+   {
+      POOL_MEM cmd;
+
+      pm_strcpy(cmd, command);
+      if (backend.ctx->write_command(ctx, cmd) < 0){
+         /* error */
+         return bRC_Error;
+      }
+
+      if (!backend.ctx->read_ack(ctx)){
+         strip_trailing_newline(cmd.c_str());
+         DMSG(ctx, DERROR, "Wrong backend response to %s command.\n", cmd.c_str());
+         JMSG(ctx, backend.ctx->jmsg_err_level(), "Wrong backend response to %s command.\n", cmd.c_str());
+         return bRC_Error;
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Send "BackupStart" protocol command.
+   *    more info at PLUGINCLASS::send_startjob
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when send command was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::send_startbackup(bpContext *ctx)
+   {
+      return send_startjob(ctx, "BackupStart\n");
+   }
+
+   /*
+   * Send "EstimateStart" protocol command.
+   *    more info at PLUGINCLASS::send_startjob
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when send command was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::send_startestimate(bpContext *ctx)
+   {
+      return send_startjob(ctx, "EstimateStart\n");
+   }
+
+   /*
+   * Send "ListingStart" protocol command.
+   *    more info at PLUGINCLASS::send_startjob
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when send command was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::send_startlisting(bpContext *ctx)
+   {
+      return send_startjob(ctx, "ListingStart\n");
+   }
+
+   /*
+   * Send "QueryStart" protocol command.
+   *    more info at PLUGIN::send_startjob
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when send command was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::send_startquery(bpContext *ctx)
+   {
+      return send_startjob(ctx, "QueryStart\n");
+   }
+
+   /*
+   * Send "RestoreStart" protocol command.
+   *    more info at PLUGINCLASS::send_startjob
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when send command was successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::send_startrestore(bpContext *ctx)
+   {
+      int32_t rc;
+      POOL_MEM cmd(PM_FNAME);
+      const char * command = "RestoreStart\n";
+      POOL_MEM extpipename(PM_FNAME);
+
+      pm_strcpy(cmd, command);
+      rc = backend.ctx->write_command(ctx, cmd);
+      if (rc < 0){
+         /* error */
+         return bRC_Error;
+      }
+
+      if (backend.ctx->read_command(ctx, cmd) < 0){
+         DMSG(ctx, DERROR, "Wrong backend response to %s command.\n", command);
+         JMSG(ctx, backend.ctx->jmsg_err_level(), "Wrong backend response to %s command.\n", command);
+         return bRC_Error;
+      }
+      if (backend.ctx->is_eod()){
+         /* got EOD so the backend is ready for restore */
+         return bRC_OK;
+      }
+
+      /* here we expect a PIPE: command only */
+      if (scan_parameter_str(cmd, "PIPE:", extpipename)){
+         /* got PIPE: */
+         DMSG(ctx, DINFO, "PIPE:%s\n", extpipename.c_str());
+         backend.ctx->set_extpipename(extpipename.c_str());
+         /* TODO: decide if plugin should verify if extpipe is available */
+         pm_strcpy(cmd, "OK\n");
+         rc = backend.ctx->write_command(ctx, cmd);
+         if (rc < 0){
+            /* error */
+            return bRC_Error;
+         }
+         return bRC_OK;
+      }
+      return bRC_Error;
+   }
+
+   /*
+   * Switches current backend context or executes new one when required.
+   *    The backend (application path) to execute is set in BACKEND_CMD compile
+   *    variable and handled by PLUGINCLASS::run_backend() method. Just before new
+   *    backend execution the method search for already spawned backends which
+   *    handles the same Plugin command and when found the current backend context
+   *    is switched to already available on list.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    command - the Plugin command for which we execute a backend
+   * out:
+   *    bRC_OK - success and backendctx has a current backend context and Plugin
+   *             should send an initialization procedure
+   *    bRC_Max - when was already prepared and initialized, so no
+   *              reinitialization required
+   *    bRC_Error - error in switching or running the backend
+   */
+   bRC PLUGINCLASS::switch_or_run_backend(bpContext *ctx, char *command)
+   {
+      DMSG0(ctx, DINFO, "Switch or run Backend.\n");
+      backend.switch_command(command);
+
+      /* check if we have the backend with the same command already */
+      if (backend.ctx->is_open())
+      {
+         /* and its open, so skip running the new */
+         DMSG0(ctx, DINFO, "Backend already prepared.\n");
+         return bRC_Max;
+      }
+
+      // now execute a backend
+      if (run_backend(ctx) != bRC_OK){
+         return bRC_Error;
+      }
+
+      return bRC_OK;
+   }
+
+
+   /*
+   * This is the main method for handling events generated by Bacula.
+   *    The behavior of the method depends on event type generated, but there are
+   *    some events which does nothing, just return with bRC_OK. Every event is
+   *    tracked in debug trace file to verify the event flow during development.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    event - a Bacula event structure
+   *    value - optional event value
+   * out:
+   *    bRC_OK - in most cases signal success/no error
+   *    bRC_Error - in most cases signal error
+   *    <other> - depend on Bacula Plugin API if applied
+   */
+   bRC PLUGINCLASS::handlePluginEvent(bpContext *ctx, bEvent *event, void *value)
+   {
+      // extract original plugin context, basically it should be `this`
+      PLUGINCLASS *pctx = (PLUGINCLASS *)ctx->pContext;
+      // this ensures that handlePluginEvent is thread safe for extracted pContext
+      // smart_lock<smart_mutex> lg(&pctx->mutex); - removed on request
+
+      if (job_cancelled) {
+         return bRC_Error;
+      }
+
+      switch (event->eventType)
+      {
+      case bEventJobStart:
+         DMSG(ctx, D3, "bEventJobStart value=%s\n", NPRT((char *)value));
+         getBaculaVar(bVarJobId, (void *)&JobId);
+         getBaculaVar(bVarJobName, (void *)&JobName);
+         if (CUSTOMPREVJOBNAME){
+            getBaculaVar(bVarPrevJobName, (void *)&prevjobname);
+         }
+         break;
+
+      case bEventJobEnd:
+         DMSG(ctx, D3, "bEventJobEnd value=%s\n", NPRT((char *)value));
+         return terminate_all_backends(ctx);
+
+      case bEventLevel:
+         char lvl;
+         lvl = (char)((intptr_t) value & 0xff);
+         DMSG(ctx, D2, "bEventLevel='%c'\n", lvl);
+         switch (lvl) {
+            case 'F':
+               DMSG0(ctx, D2, "backup level = Full\n");
+               mode = BACKUP_FULL;
+               break;
+            case 'I':
+               DMSG0(ctx, D2, "backup level = Incr\n");
+               mode = BACKUP_INCR;
+               break;
+            case 'D':
+               DMSG0(ctx, D2, "backup level = Diff\n");
+               mode = BACKUP_DIFF;
+               break;
+            default:
+               DMSG0(ctx, D2, "unsupported backup level!\n");
+               return bRC_Error;
+         }
+         break;
+
+      case bEventSince:
+         since = (time_t) value;
+         DMSG(ctx, D2, "bEventSince=%ld\n", (intptr_t) since);
+         break;
+
+      case bEventStartBackupJob:
+         DMSG(ctx, D3, "bEventStartBackupJob value=%s\n", NPRT((char *)value));
+         break;
+
+      case bEventEndBackupJob:
+         DMSG(ctx, D2, "bEventEndBackupJob value=%s\n", NPRT((char *)value));
+         break;
+
+      case bEventStartRestoreJob:
+         DMSG(ctx, DINFO, "StartRestoreJob value=%s\n", NPRT((char *)value));
+         getBaculaVar(bVarWhere, &where);
+         DMSG(ctx, DINFO, "Where=%s\n", NPRT(where));
+         getBaculaVar(bVarRegexWhere, &regexwhere);
+         DMSG(ctx, DINFO, "RegexWhere=%s\n", NPRT(regexwhere));
+         getBaculaVar(bVarReplace, &replace);
+         DMSG(ctx, DINFO, "Replace=%c\n", replace);
+         mode = RESTORE;
+         break;
+
+      case bEventEndRestoreJob:
+         DMSG(ctx, DINFO, "bEventEndRestoreJob value=%s\n", NPRT((char *)value));
+         return signal_finish_all_backends(ctx);
+
+      /* Plugin command e.g. plugin = <plugin-name>:parameters */
+      case bEventEstimateCommand:
+         DMSG(ctx, D1, "bEventEstimateCommand value=%s\n", NPRT((char *)value));
+         estimate = true;
+         return prepare_backend(ctx, BACKEND_JOB_INFO_ESTIMATE, (char*)value);
+
+      /* Plugin command e.g. plugin = <plugin-name>:parameters */
+      case bEventBackupCommand:
+         DMSG(ctx, D2, "bEventBackupCommand value=%s\n", NPRT((char *)value));
+         pluginconfigsent = false;
+         return prepare_backend(ctx, BACKEND_JOB_INFO_BACKUP, (char*)value);
+
+      /* Plugin command e.g. plugin = <plugin-name>:parameters */
+      case bEventRestoreCommand:
+         DMSG(ctx, D2, "bEventRestoreCommand value=%s\n", NPRT((char *)value));
+         return prepare_backend(ctx, BACKEND_JOB_INFO_RESTORE, (char*)value);
+
+      /* Plugin command e.g. plugin = <plugin-name>:parameters */
+      case bEventPluginCommand:
+         DMSG(ctx, D2, "bEventPluginCommand value=%s\n", NPRT((char *)value));
+         getBaculaVar(bVarAccurate, (void *)&accurate_mode);
+         if (isourplugincommand(PLUGINPREFIX, (char*)value) && !backend_available)
+         {
+            DMSG2(ctx, DERROR, "Unable to use backend: %s Err=%s\n", backend_cmd.c_str(), backend_error.c_str());
+            JMSG2(ctx, M_FATAL, "Unable to use backend: %s Err=%s\n", backend_cmd.c_str(), backend_error.c_str());
+            return bRC_Error;
+         }
+         break;
+
+      case bEventOptionPlugin:
+      case bEventHandleBackupFile:
+         if (isourplugincommand(PLUGINPREFIX, (char*)value)){
+            DMSG0(ctx, DERROR, "Invalid handle Option Plugin called!\n");
+            JMSG2(ctx, M_FATAL,
+                  "The %s plugin doesn't support the Option Plugin configuration.\n"
+                  "Please review your FileSet and move the Plugin=%s"
+                  "... command into the Include {} block.\n",
+                  PLUGINNAME, PLUGINPREFIX);
+            return bRC_Error;
+         }
+         break;
+
+      case bEventEndFileSet:
+         DMSG(ctx, D3, "bEventEndFileSet value=%s\n", NPRT((char *)value));
+         break;
+
+      case bEventRestoreObject:
+         /* Restore Object handle - a plugin configuration for restore and user supplied parameters */
+         if (!value){
+            DMSG0(ctx, DINFO, "End restore objects\n");
+            break;
+         }
+         DMSG(ctx, D2, "bEventRestoreObject value=%p\n", value);
+         return handle_plugin_restoreobj(ctx, (restore_object_pkt *) value);
+
+      case bEventCancelCommand:
+         DMSG2(ctx, D3, "bEventCancelCommand self = %p pctx = %p\n", this, pctx);
+         // TODO: PETITION: Our plugin (RHV WhiteBearSolutions) search the packet E CANCEL.
+         // TODO: If you modify this behaviour, please you notify us.
+         // TODO: RPK[20210623]: The information about a new procedure was sent to Eric
+         pctx->job_cancelled = true;
+         return cancel_all_backends(ctx);
+
+      default:
+         // enabled only for Debug
+         DMSG2(ctx, D2, "Unknown event: %s (%d) \n", eventtype2str(event), event->eventType);
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Make a real data read from the backend and checks for EOD.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    io - Bacula Plugin API I/O structure for I/O operations
+   * out:
+   *    bRC_OK - when successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::perform_read_data(bpContext *ctx, struct io_pkt *io)
+   {
+      int rc;
+
+      if (nodata){
+         io->status = 0;
+         return bRC_OK;
+      }
+      rc = backend.ctx->read_data_fixed(ctx, io->buf, io->count);
+      if (rc < 0){
+         io->status = rc;
+         io->io_errno = EIO;
+         return bRC_Error;
+      }
+      io->status = rc;
+      if (backend.ctx->is_eod()){
+         // TODO: we signal EOD as rc=0, so no need to explicity check for EOD, right?
+         io->status = 0;
+      }
+      return bRC_OK;
+   }
+
+   /*
+   * Make a real write data to the backend.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    io - Bacula Plugin API I/O structure for I/O operations
+   * out:
+   *    bRC_OK - when successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::perform_write_data(bpContext *ctx, struct io_pkt *io)
+   {
+      int rc;
+      POOL_MEM cmd(PM_FNAME);
+
+      /* check if DATA was sent */
+      if (nodata){
+         pm_strcpy(cmd, "DATA\n");
+         rc = backend.ctx->write_command(ctx, cmd.c_str());
+         if (rc < 0){
+            /* error */
+            io->status = rc;
+            io->io_errno = rc;
+            return bRC_Error;
+         }
+         /* DATA command sent */
+         nodata = false;
+      }
+      DMSG1(ctx, DVDEBUG, "perform_write_data: %d\n", io->count);
+      rc = backend.ctx->write_data(ctx, io->buf, io->count);
+      io->status = rc;
+      if (rc < 0){
+         io->io_errno = rc;
+         return bRC_Error;
+      }
+      nodata = false;
+      return bRC_OK;
+   }
+
+   /*
+   * Handle protocol "DATA" command from backend during backup loop.
+   *    It handles a "no data" flag when saved file contains no data to
+   *    backup (empty file).
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    io - Bacula Plugin API I/O structure for I/O operations
+   * out:
+   *    bRC_OK - when successful
+   *    bRC_Error - on any error
+   *    io->status, io->io_errno - set to error on any error
+   */
+   bRC PLUGINCLASS::perform_backup_open(bpContext *ctx, struct io_pkt *io)
+   {
+      int rc;
+      POOL_MEM cmd(PM_FNAME);
+
+      /* expecting DATA command */
+      nodata = false;
+      rc = backend.ctx->read_command(ctx, cmd);
+      if (backend.ctx->is_eod()){
+         /* no data for file */
+         nodata = true;
+      } else
+      // expect no error and 'DATA' starting packet
+      if (rc < 0 || !bstrcmp(cmd.c_str(), "DATA")){
+         io->status = rc;
+         io->io_errno = EIO;
+         openerror = backend.ctx->is_fatal() ? false : true;
+         return bRC_Error;
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Signal the end of data to restore and verify acknowledge from backend.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    io - Bacula Plugin API I/O structure for I/O operations
+   * out:
+   *    bRC_OK - when successful
+   *    bRC_Error - on any error
+   */
+   bRC PLUGINCLASS::perform_write_end(bpContext *ctx, struct io_pkt *io)
+   {
+      if (!nodata){
+         /* signal end of data to restore and get ack */
+         if (!backend.ctx->send_ack(ctx)){
+            io->status = -1;
+            io->io_errno = EPIPE;
+            return bRC_Error;
+         }
+      }
+
+      if (last_type == FT_DIREND) {
+         struct xacl_pkt xacl;
+
+         if (acldatalen > 0) {
+            xacl.count = acldatalen;
+            xacl.content = acldata.c_str();
+            bRC status = perform_write_acl(ctx, &xacl);
+            if (status != bRC_OK){
+               return status;
+            }
+         }
+         if (xattrdatalen > 0) {
+            xacl.count = xattrdatalen;
+            xacl.content = xattrdata.c_str();
+            bRC status = perform_write_xattr(ctx, &xacl);
+            if (status != bRC_OK){
+               return status;
+            }
+         }
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Reads ACL data from backend during backup. Save it for handleXACLdata from
+   * Bacula. As we do not know if a backend will send the ACL data or not, we
+   * cannot wait to read this data until handleXACLdata will be called.
+   * TODO: The method has a limitation and accept up to PM_BSOCK acl data to save
+   *       which is about 64kB. It should be sufficient for virtually all acl data
+   *       we can imagine. But when a backend developer could expect a larger data
+   *       to save he should rewrite perform_read_acl() method.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when ACL data was read successfully and this.readacl set to true
+   *    bRC_Error - on any error during acl data read
+   */
+   bRC PLUGINCLASS::perform_read_acl(bpContext *ctx)
+   {
+      DMSG0(ctx, DINFO, "perform_read_acl\n");
+      acldatalen = backend.ctx->read_data(ctx, acldata);
+      if (acldatalen < 0){
+         DMSG0(ctx, DERROR, "Cannot read ACL data from backend.\n");
+         return bRC_Error;
+      }
+
+      DMSG1(ctx, DINFO, "readACL: %i\n", acldatalen);
+      if (!backend.ctx->read_ack(ctx)){
+         /* should get EOD */
+         DMSG0(ctx, DERROR, "Protocol error, should get EOD.\n");
+         return bRC_Error;
+      }
+
+      readacl = true;
+
+      return bRC_OK;
+   }
+
+   /*
+   * Reads XATTR data from backend during backup. Save it for handleXACLdata from
+   * Bacula. As we do not know if a backend will send the XATTR data or not, we
+   * cannot wait to read this data until handleXACLdata will be called.
+   * TODO: The method has a limitation and accept up to PM_BSOCK xattr data to save
+   *       which is about 64kB. It should be sufficient for virtually all xattr data
+   *       we can imagine. But when a backend developer could expect a larger data
+   *       to save he should rewrite perform_read_xattr() method.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when XATTR data was read successfully and this.readxattr set
+   *             to true
+   *    bRC_Error - on any error during acl data read
+   */
+   bRC PLUGINCLASS::perform_read_xattr(bpContext *ctx)
+   {
+      DMSG0(ctx, DINFO, "perform_read_xattr\n");
+      xattrdatalen = backend.ctx->read_data(ctx, xattrdata);
+      if (xattrdatalen < 0){
+         DMSG0(ctx, DERROR, "Cannot read XATTR data from backend.\n");
+         return bRC_Error;
+      }
+      DMSG1(ctx, DINFO, "readXATTR: %i\n", xattrdatalen);
+      if (!backend.ctx->read_ack(ctx)){
+         /* should get EOD */
+         DMSG0(ctx, DERROR, "Protocol error, should get EOD.\n");
+         return bRC_Error;
+      }
+      readxattr = true;
+      return bRC_OK;
+   }
+
+   /**
+    * @brief Reads metadata info from backend and adds it as a metadata packet.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param type detected Metadata type
+    * @param sp save packet
+    * @return bRC bRC_OK when success, bRC_Error when some error
+    */
+   bRC PLUGINCLASS::perform_read_metadata_info(bpContext *ctx, metadata_type type, struct save_pkt *sp)
+   {
+      POOL_MEM data(PM_MESSAGE);
+
+      DMSG0(ctx, DINFO, "perform_read_metadata_info\n");
+
+      int len = backend.ctx->read_data(ctx, data);
+      if (len < 0){
+         DMSG1(ctx, DERROR, "Cannot read METADATA(%i) information from backend.\n", type);
+         return bRC_Error;
+      }
+
+      DMSG1(ctx, DINFO, "read METADATA info len: %i\n", len);
+      if (!backend.ctx->read_ack(ctx)){
+         /* should get EOD */
+         DMSG0(ctx, DERROR, "Protocol error, should get EOD.\n");
+         return bRC_Error;
+      }
+
+      // Bacula API for metadata requires that a plugin
+      // handle metadata buffer allocation
+      POOLMEM *ptr = (POOLMEM *)bmalloc(len);
+      memcpy(ptr, data.addr(), len);
+
+      // add it to the list for reference to not lot it
+      metadatas_list.append(ptr);
+      metadatas.add_packet(type, len, ptr);
+      sp->plug_meta = &metadatas;
+
+      return bRC_OK;
+   }
+
+   /**
+    * @brief Does metadata command scan and map to metadata types.
+    *
+    * @param cmd a command string read from backend
+    * @return metadata_type returned from map
+    */
+   metadata_type PLUGINCLASS::scan_metadata_type(bpContext *ctx, const POOL_MEM &cmd)
+   {
+      DMSG1(ctx, DDEBUG, "scan_metadata_type checking: %s\n", cmd.c_str());
+      for (int i = 0; plugin_metadata_map[i].command != NULL; i++)
+      {
+         if (bstrcmp(cmd.c_str(), plugin_metadata_map[i].command)){
+            DMSG2(ctx, DDEBUG, "match: %s => %d\n", plugin_metadata_map[i].command, plugin_metadata_map[i].type);
+            return plugin_metadata_map[i].type;
+         }
+      }
+
+      return plugin_meta_invalid;
+   }
+
+   const char * PLUGINCLASS::prepare_metadata_type(metadata_type type)
+   {
+      for (int i = 0; plugin_metadata_map[i].command != NULL; i++){
+         if (plugin_metadata_map[i].type == type){
+            return plugin_metadata_map[i].command;
+         }
+      }
+
+      return "METADATA_STREAM\n";
+   }
+
+   /*
+   * Sends ACL data from restore stream to backend.
+   * TODO: The method has a limitation and accept a single xacl_pkt call for
+   *       a single file. As the restored acl stream and records are the same as
+   *       was saved during backup, you can expect no more then a single PM_BSOCK
+   *       and about 64kB of acl data send to backend.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    xacl_pkt - the restored ACL data for backend
+   * out:
+   *    bRC_OK - when ACL data was restored successfully
+   *    bRC_Error - on any error during acl data restore
+   */
+   bRC PLUGINCLASS::perform_write_acl(bpContext* ctx, const xacl_pkt* xacl)
+   {
+      if (xacl->count > 0) {
+         POOL_MEM cmd(PM_FNAME);
+         /* send command ACL */
+         pm_strcpy(cmd, "ACL\n");
+         backend.ctx->write_command(ctx, cmd.c_str());
+         /* send acls data */
+         DMSG1(ctx, DINFO, "writeACL: %i\n", xacl->count);
+         int rc = backend.ctx->write_data(ctx, xacl->content, xacl->count);
+         if (rc < 0) {
+            /* got some error */
+            return bRC_Error;
+         }
+         /* signal end of acls data to restore and get ack */
+         if (!backend.ctx->send_ack(ctx)) {
+            return bRC_Error;
+         }
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Sends XATTR data from restore stream to backend.
+   * TODO: The method has a limitation and accept a single xacl_pkt call for
+   *       a single file. As the restored acl stream and records are the same as
+   *       was saved during backup, you can expect no more then a single PM_BSOCK
+   *       and about 64kB of acl data send to backend.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    xacl_pkt - the restored XATTR data for backend
+   * out:
+   *    bRC_OK - when XATTR data was restored successfully
+   *    bRC_Error - on any error during acl data restore
+   */
+   bRC PLUGINCLASS::perform_write_xattr(bpContext* ctx, const xacl_pkt* xacl)
+   {
+      if (xacl->count > 0) {
+         POOL_MEM cmd(PM_FNAME);
+         /* send command XATTR */
+         pm_strcpy(cmd, "XATTR\n");
+         backend.ctx->write_command(ctx, cmd.c_str());
+         /* send xattrs data */
+         DMSG1(ctx, DINFO, "writeXATTR: %i\n", xacl->count);
+         int rc = backend.ctx->write_data(ctx, xacl->content, xacl->count);
+         if (rc < 0) {
+            /* got some error */
+            return bRC_Error;
+         }
+         /* signal end of xattrs data to restore and get ack */
+         if (!backend.ctx->send_ack(ctx)) {
+            return bRC_Error;
+         }
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * The method works as a dispatcher for expected commands received from backend.
+   *    It handles a three commands associated with file attributes/metadata:
+   *    - FNAME:... - the next file to backup
+   *    - ACL - next data will be acl data, so perform_read_acl()
+   *    - XATTR - next data will be xattr data, so perform_read_xattr()
+   *    and additionally when no more files to backup it handles EOD.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   * out:
+   *    bRC_OK - when plugin read the command, dispatched a work and setup flags
+   *    bRC_Error - on any error during backup
+   */
+   bRC PLUGINCLASS::perform_read_metacommands(bpContext *ctx)
+   {
+      POOL_MEM cmd(PM_FNAME);
+
+      DMSG0(ctx, DDEBUG, "perform_read_metacommands()\n");
+      // setup flags
+      nextfile = readacl = readxattr = false;
+      objectsent = false;
+      // loop on metadata from backend or EOD which means no more files to backup
+      while (true)
+      {
+         if (backend.ctx->read_command(ctx, cmd) > 0){
+            /* yup, should read FNAME, ACL or XATTR from backend, check which one */
+            DMSG(ctx, DDEBUG, "read_command(1): %s\n", cmd.c_str());
+            if (scan_parameter_str(cmd, "FNAME:", fname)){
+               /* got FNAME: */
+               nextfile = true;
+               object = FileObject;
+               return bRC_OK;
+            }
+            if (scan_parameter_str(cmd, "PLUGINOBJ:", fname)){
+               /* got Plugin Object header */
+               nextfile = true;
+               object = PluginObject;
+               // pluginobject = true;
+               return bRC_OK;
+            }
+            if (scan_parameter_str(cmd, "RESTOREOBJ:", fname)){
+               /* got Restore Object header */
+               nextfile = true;
+               object = RestoreObject;
+               // restoreobject = true;
+               return bRC_OK;
+            }
+            if (scan_parameter_str(cmd, "CHECK:", fname)){
+               /* got accurate check query */
+               perform_accurate_check(ctx);
+               continue;
+            }
+            if (scan_parameter_str(cmd, "CHECKGET:", fname)){
+               /* got accurate get query */
+               perform_accurate_check_get(ctx);
+               continue;
+            }
+            if (bstrcmp(cmd.c_str(), "ACL")){
+               /* got ACL header */
+               perform_read_acl(ctx);
+               continue;
+            }
+            if (bstrcmp(cmd.c_str(), "XATTR")){
+               /* got XATTR header */
+               perform_read_xattr(ctx);
+               continue;
+            }
+            if (bstrcmp(cmd.c_str(), "FileIndex")){
+               /* got FileIndex query */
+               perform_file_index_query(ctx);
+               continue;
+            }
+            /* error in protocol */
+            DMSG(ctx, DERROR, "Protocol error, got unknown command: %s\n", cmd.c_str());
+            JMSG(ctx, M_FATAL, "Protocol error, got unknown command: %s\n", cmd.c_str());
+            return bRC_Error;
+         } else {
+            if (backend.ctx->is_fatal()){
+               /* raise up error from backend */
+               return bRC_Error;
+            }
+            if (backend.ctx->is_eod()){
+               /* no more files to backup */
+               DMSG0(ctx, DDEBUG, "No more files to backup from backend.\n");
+               return bRC_OK;
+            }
+         }
+      }
+
+      return bRC_Error;
+   }
+
+   /**
+    * @brief Respond to the file index query command from backend.
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @return bRC bRC_OK when success, bRC_Error if not
+    */
+   bRC PLUGINCLASS::perform_file_index_query(bpContext *ctx)
+   {
+      POOL_MEM cmd(PM_FNAME);
+      int32_t fileindex;
+
+      getBaculaVar(bVarFileIndex, (void *)&fileindex);
+      Mmsg(cmd, "%d\n", fileindex);
+      if (backend.ctx->write_command(ctx, cmd) < 0){
+         /* error */
+         return bRC_Error;
+      }
+
+      return bRC_OK;
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @return bRC bRC_OK when success, bRC_Error if not
+    */
+   bRC PLUGINCLASS::perform_accurate_check(bpContext *ctx)
+   {
+      if (strlen(fname.c_str()) == 0){
+         // input variable is not valid
+         return bRC_Error;
+      }
+
+      DMSG0(ctx, DDEBUG, "perform_accurate_check()\n");
+
+      POOL_MEM cmd(PM_FNAME);
+      struct save_pkt sp;
+      memset(&sp, 0, sizeof(sp));
+
+      // supported sequence is `STAT` followed by `TSTAMP`
+      if (backend.ctx->read_command(ctx, cmd) < 0) {
+         // error
+         return bRC_Error;
+      }
+
+      metaplugin::attributes::Status status = metaplugin::attributes::read_scan_stat_command(ctx, cmd, &sp);
+      if (status == metaplugin::attributes::Status_OK) {
+         if (backend.ctx->read_command(ctx, cmd) < 0) {
+            // error
+            return bRC_Error;
+         }
+
+         status = metaplugin::attributes::read_scan_tstamp_command(ctx, cmd, &sp);
+         if (status == metaplugin::attributes::Status_OK) {
+            // success we can perform accurate check for stat packet
+            bRC rc = bRC_OK;  // return 'OK' as a default
+            if (accurate_mode) {
+               sp.fname = fname.c_str();
+               rc = checkChanges(&sp);
+            } else {
+               if (!accurate_mode_err) {
+                  DMSG0(ctx, DERROR, "Backend CHECK command require accurate mode on!\n");
+                  JMSG0(ctx, M_ERROR, "Backend CHECK command require accurate mode on!\n");
+                  accurate_mode_err = true;
+               }
+            }
+
+            POOL_MEM checkstatus(PM_NAME);
+            Mmsg(checkstatus, "%s\n", rc == bRC_Seen ? "SEEN" : "OK");
+            DMSG1(ctx, DINFO, "perform_accurate_check(): %s", checkstatus.c_str());
+
+            if (!backend.ctx->write_command(ctx, checkstatus)) {
+               DMSG0(ctx, DERROR, "Cannot send checkChanges() response to backend\n");
+               JMSG0(ctx, backend.ctx->jmsg_err_level(), "Cannot send checkChanges() response to backend\n");
+               return bRC_Error;
+            }
+
+            return bRC_OK;
+         }
+      } else {
+         // check possible errors
+         switch (status)
+         {
+         case metaplugin::attributes::Invalid_File_Type:
+            JMSG2(ctx, M_ERROR, "Invalid file type: %c for %s\n", sp.type, fname.c_str());
+            return bRC_Error;
+
+         case metaplugin::attributes::Invalid_Stat_Packet:
+            JMSG1(ctx, backend.ctx->jmsg_err_level(), "Invalid stat packet: %s\n", cmd.c_str());
+            return bRC_Error;
+         default:
+            break;
+         }
+         // future extension for `ATTR` command
+         // ...
+      }
+
+      return bRC_Error;
+   }
+
+   /**
+    * @brief Perform accurate query check and resturn accurate data to backend.
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @return bRC bRC_OK when success, bRC_Error if not
+    */
+   bRC PLUGINCLASS::perform_accurate_check_get(bpContext *ctx)
+   {
+      POOL_MEM cmd(PM_FNAME);
+
+      if (strlen(fname.c_str()) == 0){
+         // input variable is not valid
+         return bRC_Error;
+      }
+
+      DMSG0(ctx, DDEBUG, "perform_accurate_check_get()\n");
+
+      if (!accurate_mode) {
+         // the job is not accurate, so no accurate data will be available at all
+         pm_strcpy(cmd, "NOACCJOB\n");
+         if (!backend.ctx->signal_error(ctx, cmd)) {
+            DMSG0(ctx, DERROR, "Cannot send 'No Accurate Job' info to backend\n");
+            JMSG0(ctx, backend.ctx->jmsg_err_level(), "Cannot send 'No Accurate Job' info to backend\n");
+            return bRC_Error;
+         }
+         return bRC_OK;
+      }
+
+      accurate_attribs_pkt attribs;
+      memset(&attribs, 0, sizeof(attribs));
+
+      attribs.fname = fname.c_str();
+      bRC rc = getAccurateAttribs(&attribs);
+
+      struct restore_pkt rp;
+
+      switch (rc)
+      {
+      case bRC_Seen:
+         memcpy(&rp.statp, &attribs.statp, sizeof(rp.statp));
+         rp.type = FT_MASK;   // This is a special metaplugin protocol hack
+                              // because the current Bacula accurate code does
+                              // not handle FileType on catalog attributes, yet.
+         // STAT:...
+         metaplugin::attributes::make_stat_command(ctx, cmd, &rp);
+         backend.ctx->write_command(ctx, cmd);
+
+         // TSTAMP:...
+         if (metaplugin::attributes::make_tstamp_command(ctx, cmd, &rp) == metaplugin::attributes::Status_OK) {
+            backend.ctx->write_command(ctx, cmd);
+            DMSG(ctx, DINFO, "createFile:%s", cmd.c_str());
+         }
+
+         break;
+      default:
+         pm_strcpy(cmd, "UNAVAIL\n");
+         if (!backend.ctx->write_command(ctx, cmd)) {
+            DMSG0(ctx, DERROR, "Cannot send 'UNAVAIL' response to backend\n");
+            JMSG0(ctx, backend.ctx->jmsg_err_level(), "Cannot send 'UNAVAIL' response to backend\n");
+            return bRC_Error;
+         }
+         break;
+      }
+
+      return bRC_OK;
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @param sp save_pkt from startBackupFile()
+    * @return bRC bRC_OK when success, bRC_Error if not
+    */
+   bRC PLUGINCLASS::perform_read_pluginobject(bpContext *ctx, struct save_pkt *sp)
+   {
+      POOL_MEM cmd(PM_FNAME);
+
+      if (strlen(fname.c_str()) == 0){
+         // input variable is not valid
+         return bRC_Error;
+      }
+
+      sp->plugin_obj.path = fname.c_str();
+      DMSG0(ctx, DDEBUG, "perform_read_pluginobject()\n");
+      // loop on plugin objects parameters from backend and EOD
+      while (true){
+         if (backend.ctx->read_command(ctx, cmd) > 0){
+            DMSG(ctx, DDEBUG, "read_command(3): %s\n", cmd.c_str());
+            if (scan_parameter_str(cmd, "PLUGINOBJ_CAT:", plugin_obj_cat)){
+               DMSG1(ctx, DDEBUG, "category: %s\n", plugin_obj_cat.c_str());
+               sp->plugin_obj.object_category = plugin_obj_cat.c_str();
+               continue;
+            }
+            if (scan_parameter_str(cmd, "PLUGINOBJ_TYPE:", plugin_obj_type)){
+               DMSG1(ctx, DDEBUG, "type: %s\n", plugin_obj_type.c_str());
+               sp->plugin_obj.object_type = plugin_obj_type.c_str();
+               continue;
+            }
+            if (scan_parameter_str(cmd, "PLUGINOBJ_NAME:", plugin_obj_name)){
+               DMSG1(ctx, DDEBUG, "name: %s\n", plugin_obj_name.c_str());
+               sp->plugin_obj.object_name = plugin_obj_name.c_str();
+               continue;
+            }
+            if (scan_parameter_str(cmd, "PLUGINOBJ_SRC:", plugin_obj_src)){
+               DMSG1(ctx, DDEBUG, "src: %s\n", plugin_obj_src.c_str());
+               sp->plugin_obj.object_source = plugin_obj_src.c_str();
+               continue;
+            }
+            if (scan_parameter_str(cmd, "PLUGINOBJ_UUID:", plugin_obj_uuid)){
+               DMSG1(ctx, DDEBUG, "uuid: %s\n", plugin_obj_uuid.c_str());
+               sp->plugin_obj.object_uuid = plugin_obj_uuid.c_str();
+               continue;
+            }
+            POOL_MEM param(PM_NAME);
+            if (scan_parameter_str(cmd, "PLUGINOBJ_SIZE:", param)){
+               if (!size_to_uint64(param.c_str(), strlen(param.c_str()), &plugin_obj_size)){
+                  // error in convert
+                  DMSG1(ctx, DERROR, "Cannot convert Plugin Object Size to integer! p=%s\n", param.c_str());
+                  JMSG1(ctx, M_ERROR, "Cannot convert Plugin Object Size to integer! p=%s\n", param.c_str());
+                  return bRC_Error;
+               }
+               DMSG1(ctx, DDEBUG, "size: %llu\n", plugin_obj_size);
+               sp->plugin_obj.object_size = plugin_obj_size;
+               continue;
+            }
+            if (scan_parameter_str(cmd, "PLUGINOBJ_COUNT:", param)){
+               uint32_t count = str_to_int64(param.c_str());
+               DMSG1(ctx, DDEBUG, "count: %lu\n", count);
+               sp->plugin_obj.count = count;
+               continue;
+            }
+            /* error in protocol */
+            DMSG(ctx, DERROR, "Protocol error, got unknown command: %s\n", cmd.c_str());
+            JMSG(ctx, M_FATAL, "Protocol error, got unknown command: %s\n", cmd.c_str());
+            return bRC_Error;
+         } else {
+            if (backend.ctx->is_fatal()){
+               /* raise up error from backend */
+               return bRC_Error;
+            }
+            if (backend.ctx->is_eod()){
+               /* no more plugin object params to backup */
+               DMSG0(ctx, DINFO, "No more Plugin Object params from backend.\n");
+               // pluginobject = false;
+               // pluginobjectsent = true;
+               objectsent = true;
+               return bRC_OK;
+            }
+         }
+      }
+
+      return bRC_Error;
+   }
+
+   /**
+    * @brief Receives a Restore Object data and populates save_pkt.
+    *
+    * @param ctx bpContext - for Bacula debug and jobinfo messages
+    * @param sp save_pkt from startBackupFile()
+    * @return bRC bRC_OK when success, bRC_Error if not
+    */
+   bRC PLUGINCLASS::perform_read_restoreobject(bpContext *ctx, struct save_pkt *sp)
+   {
+      POOL_MEM cmd(PM_FNAME);
+
+      sp->restore_obj.object = NULL;
+
+      if (strlen(fname.c_str()) == 0){
+         // input variable is not valid
+         return bRC_Error;
+      }
+
+      DMSG0(ctx, DDEBUG, "perform_read_restoreobject()\n");
+      // read object length required param
+      if (backend.ctx->read_command(ctx, cmd) > 0) {
+         DMSG(ctx, DDEBUG, "read_command(4): %s\n", cmd.c_str());
+         POOL_MEM param(PM_NAME);
+         uint64_t length;
+         if (scan_parameter_str(cmd, "RESTOREOBJ_LEN:", param)) {
+            if (!size_to_uint64(param.c_str(), strlen(param.c_str()), &length)){
+               // error in convert
+               DMSG1(ctx, DERROR, "Cannot convert Restore Object length to integer! p=%s\n", param.c_str());
+               JMSG1(ctx, M_ERROR, "Cannot convert Restore Object length to integer! p=%s\n", param.c_str());
+               return bRC_Error;
+            }
+            DMSG1(ctx, DDEBUG, "size: %llu\n", length);
+            sp->restore_obj.object_len = length;
+            robjbuf.check_size(length + 1);
+         } else {
+            // no required param
+            DMSG0(ctx, DERROR, "Cannot read Restore Object length!\n");
+            JMSG0(ctx, M_ERROR, "Cannot read Restore Object length!\n");
+            return bRC_Error;
+         }
+      } else {
+         if (backend.ctx->is_fatal()){
+            /* raise up error from backend */
+            return bRC_Error;
+         }
+      }
+
+      int32_t recv_len = 0;
+
+      if (backend.ctx->recv_data(ctx, robjbuf, &recv_len) != bRC_OK) {
+         DMSG0(ctx, DERROR, "Cannot read data from backend!\n");
+         return bRC_Error;
+      }
+
+      /* no more restore object data to backup */
+      DMSG0(ctx, DINFO, "No more Restore Object data from backend.\n");
+      objectsent = true;
+
+      if (recv_len != sp->restore_obj.object_len) {
+         DMSG2(ctx, DERROR, "Backend reported RO length:%ld read:%ld\n", sp->restore_obj.object_len, recv_len);
+         JMSG2(ctx, M_ERROR, "Backend reported RO length:%ld read:%ld\n", sp->restore_obj.object_len, recv_len);
+         sp->restore_obj.object_len = recv_len;
+      }
+
+      sp->restore_obj.object = robjbuf.c_str();
+
+      return bRC_OK;
+   }
+
+   /*
+   * Handle Bacula Plugin I/O API for backend
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    io - Bacula Plugin API I/O structure for I/O operations
+   * out:
+   *    bRC_OK - when successful
+   *    bRC_Error - on any error
+   *    io->status, io->io_errno - correspond to a plugin io operation status
+   */
+   bRC PLUGINCLASS::pluginIO(bpContext *ctx, struct io_pkt *io)
+   {
+      static int rw = 0;      // this variable handles single debug message
+
+      {
+         // synchronie access to job_cancelled variable
+         // smart_lock<smart_mutex> lg(&mutex); - removed on request
+         if (job_cancelled) {
+            return bRC_Error;
+         }
+      }
+
+      /* assume no error from the very beginning */
+      io->status = 0;
+      io->io_errno = 0;
+      switch (io->func) {
+         case IO_OPEN:
+            DMSG(ctx, D2, "IO_OPEN: (%s)\n", io->fname);
+            switch (mode){
+               case BACKUP_FULL:
+               case BACKUP_INCR:
+               case BACKUP_DIFF:
+                  return perform_backup_open(ctx, io);
+               case RESTORE:
+                  nodata = true;
+                  break;
+               default:
+                  return bRC_Error;
+            }
+            break;
+         case IO_READ:
+            if (!rw) {
+               rw = 1;
+               DMSG2(ctx, D2, "IO_READ buf=%p len=%d\n", io->buf, io->count);
+            }
+            switch (mode){
+               case BACKUP_FULL:
+               case BACKUP_INCR:
+               case BACKUP_DIFF:
+                  return perform_read_data(ctx, io);
+               default:
+                  return bRC_Error;
+            }
+            break;
+         case IO_WRITE:
+            if (!rw) {
+               rw = 1;
+               DMSG2(ctx, D2, "IO_WRITE buf=%p len=%d\n", io->buf, io->count);
+            }
+            switch (mode){
+               case RESTORE:
+                  return perform_write_data(ctx, io);
+               default:
+                  return bRC_Error;
+            }
+            break;
+         case IO_CLOSE:
+            DMSG0(ctx, D2, "IO_CLOSE\n");
+            rw = 0;
+            if (!backend.ctx->close_extpipe(ctx)){
+               return bRC_Error;
+            }
+            switch (mode){
+               case RESTORE:
+                  return perform_write_end(ctx, io);
+               case BACKUP_FULL:
+               case BACKUP_INCR:
+               case BACKUP_DIFF:
+                  return perform_read_metacommands(ctx);
+               default:
+                  return bRC_Error;
+            }
+            break;
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Get all required information from backend to populate save_pkt for Bacula.
+   *    It handles a Restore Object (FT_PLUGIN_CONFIG) for every Full backup and
+   *    new Plugin Backup Command if setup in FileSet. It uses a help from
+   *    endBackupFile() handling the next FNAME command for the next file to
+   *    backup. The communication protocol requires some file attributes command
+   *    required it raise the error when insufficient parameters received from
+   *    backend. It assumes some parameters at save_pkt struct to be automatically
+   *    set like: sp->portable, sp->statp.st_blksize, sp->statp.st_blocks.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    save_pkt - Bacula Plugin API save packet structure
+   * out:
+   *    bRC_OK - when save_pkt prepared successfully and we have file to backup
+   *    bRC_Max - when no more files to backup
+   *    bRC_Error - in any error
+   */
+   bRC PLUGINCLASS::startBackupFile(bpContext *ctx, struct save_pkt *sp)
+   {
+      POOL_MEM cmd(PM_FNAME);
+      int reqparams = 2;
+
+      if (job_cancelled) {
+         return bRC_Error;
+      }
+
+      /* The first file in Full backup, is the RestoreObject */
+      if (!estimate && mode == BACKUP_FULL && pluginconfigsent == false) {
+         ConfigFile ini;
+         ini.register_items(plugin_items_dump, sizeof(struct ini_items));
+         sp->restore_obj.object_name = (char *)INI_RESTORE_OBJECT_NAME;
+         sp->restore_obj.object_len = ini.serialize(robjbuf.handle());
+         sp->restore_obj.object = robjbuf.c_str();
+         sp->type = FT_PLUGIN_CONFIG;
+         DMSG2(ctx, DINFO, "Prepared RestoreObject/%s (%d) sent.\n", INI_RESTORE_OBJECT_NAME, FT_PLUGIN_CONFIG);
+         return bRC_OK;
+      }
+
+      // check if this is the first file from backend to backup
+      if (!nextfile){
+         // so read FNAME or EOD/Error
+         if (perform_read_metacommands(ctx) != bRC_OK){
+            // signal error
+            return bRC_Error;
+         }
+         if (!nextfile){
+            // got EOD, so no files to backup at all!
+            // if we return a value different from bRC_OK then Bacula will finish
+            // backup process, which at first call means no files to archive
+            return bRC_Max;
+         }
+      }
+      // setup required fname in save_pkt
+      DMSG(ctx, DINFO, "fname:%s\n", fname.c_str());
+      sp->fname = fname.c_str();
+
+      switch (object)
+      {
+      case RestoreObject:
+         // handle Restore Object parameters and data
+         if (perform_read_restoreobject(ctx, sp) != bRC_OK) {
+            // signal error
+            return bRC_Error;
+         }
+         sp->restore_obj.object_name = fname.c_str();
+         sp->type = FT_RESTORE_FIRST;
+         sp->statp.st_size = sp->restore_obj.object_len;
+         sp->statp.st_mode = 0700 | S_IFREG;
+         {
+            time_t now = time(NULL);
+            sp->statp.st_ctime = now;
+            sp->statp.st_mtime = now;
+            sp->statp.st_atime = now;
+         }
+         break;
+      case PluginObject:
+         // handle Plugin Object parameters
+         if (perform_read_pluginobject(ctx, sp) != bRC_OK) {
+            // signal error
+            return bRC_Error;
+         }
+         sp->type = FT_PLUGIN_OBJECT;
+         sp->statp.st_size = sp->plugin_obj.object_size;
+         break;
+      default:
+         // here we handle standard file metadata information
+         reqparams--;
+
+         // ensure clear state for metadatas
+         sp->plug_meta = NULL;
+         metadatas.reset();
+         metadatas_list.destroy();
+
+         while (backend.ctx->read_command(ctx, cmd) > 0)
+         {
+            DMSG(ctx, DINFO, "read_command(2): %s\n", cmd.c_str());
+            metaplugin::attributes::Status status = metaplugin::attributes::read_scan_stat_command(ctx, cmd, sp);
+            switch (status)
+            {
+            case metaplugin::attributes::Invalid_File_Type:
+               JMSG2(ctx, M_ERROR, "Invalid file type: %c for %s\n", sp->type, fname.c_str());
+               return bRC_Error;
+
+            case metaplugin::attributes::Invalid_Stat_Packet:
+               JMSG1(ctx, backend.ctx->jmsg_err_level(), "Invalid stat packet: %s\n", cmd.c_str());
+               return bRC_Error;
+
+            case metaplugin::attributes::Status_OK:
+               if (sp->type != FT_LNK) {
+                  reqparams--;
+               }
+               continue;
+            default:
+               break;
+            }
+            status = metaplugin::attributes::read_scan_tstamp_command(ctx, cmd, sp);
+            switch (status)
+            {
+            case metaplugin::attributes::Status_OK:
+               continue;
+            default:
+               break;
+            }
+            if (scan_parameter_str(cmd, "LSTAT:", lname) == 1) {
+               sp->link = lname.c_str();
+               reqparams--;
+               DMSG(ctx, DINFO, "LSTAT:%s\n", lname.c_str());
+               continue;
+            }
+            POOL_MEM tmp(PM_FNAME);
+            if (scan_parameter_str(cmd, "PIPE:", tmp)) {
+               /* handle PIPE command */
+               DMSG(ctx, DINFO, "read pipe at: %s\n", tmp.c_str());
+               int extpipe = open(tmp.c_str(), O_RDONLY);
+               if (extpipe > 0) {
+                  DMSG0(ctx, DINFO, "ExtPIPE file available.\n");
+                  backend.ctx->set_extpipe(extpipe);
+                  pm_strcpy(tmp, "OK\n");
+                  backend.ctx->write_command(ctx, tmp.c_str());
+               } else {
+                  /* here are common error signaling */
+                  berrno be;
+                  DMSG(ctx, DERROR, "ExtPIPE file open error! Err=%s\n", be.bstrerror());
+                  JMSG(ctx, backend.ctx->jmsg_err_level(), "ExtPIPE file open error! Err=%s\n", be.bstrerror());
+                  pm_strcpy(tmp, "Err\n");
+                  backend.ctx->signal_error(ctx, tmp.c_str());
+                  return bRC_Error;
+               }
+               continue;
+            }
+            metadata_type mtype = scan_metadata_type(ctx, cmd);
+            if (mtype != plugin_meta_invalid) {
+               DMSG1(ctx, DDEBUG, "metaData handling: %d\n", mtype);
+               if (perform_read_metadata_info(ctx, mtype, sp) != bRC_OK) {
+                  DMSG0(ctx, DERROR, "Cannot perform_read_metadata_info!\n");
+                  JMSG0(ctx, backend.ctx->jmsg_err_level(), "Cannot perform_read_metadata_info!\n");
+                  return bRC_Error;
+               }
+               continue;
+            } else {
+               DMSG1(ctx, DERROR, "Invalid File Attributes command: %s\n", cmd.c_str());
+               JMSG1(ctx, backend.ctx->jmsg_err_level(), "Invalid File Attributes command: %s\n", cmd.c_str());
+               return bRC_Error;
+            }
+         }
+
+         DMSG0(ctx, DINFO, "File attributes end.\n");
+         if (reqparams > 0) {
+            DMSG0(ctx, DERROR, "Protocol error, not enough file attributes from backend.\n");
+            JMSG0(ctx, M_FATAL, "Protocol error, not enough file attributes from backend.\n");
+            return bRC_Error;
+         }
+
+         break;
+      }
+
+      if (backend.ctx->is_error()) {
+         return bRC_Error;
+      }
+
+      sp->portable = true;
+      sp->statp.st_blksize = 4096;
+      sp->statp.st_blocks = sp->statp.st_size / 4096 + 1;
+
+      DMSG3(ctx, DINFO, "TSDebug: %ld(at) %ld(mt) %ld(ct)\n",
+            sp->statp.st_atime, sp->statp.st_mtime, sp->statp.st_ctime);
+
+      return bRC_OK;
+   }
+
+   /*
+   * Check for a next file to backup or the end of the backup loop.
+   *    The next file to backup is indicated by a FNAME command from backend and
+   *    no more files to backup as EOD. It helps startBackupFile handling FNAME
+   *    for next file.
+   *
+   * in:
+   *    bpContext - for Bacula debug and jobinfo messages
+   *    save_pkt - Bacula Plugin API save packet structure
+   * out:
+   *    bRC_OK - when no more files to backup
+   *    bRC_More - when Bacula should expect a next file
+   *    bRC_Error - in any error
+   */
+   bRC PLUGINCLASS::endBackupFile(bpContext *ctx)
+   {
+      POOL_MEM cmd(PM_FNAME);
+
+      {
+         // synchronie access to job_cancelled variable
+         // smart_lock<smart_mutex> lg(&mutex); - removed on request
+         if (job_cancelled) {
+            return bRC_Error;
+         }
+      }
+
+      if (!estimate){
+         /* The current file was the restore object, so just ask for the next file */
+         if (mode == BACKUP_FULL && pluginconfigsent == false) {
+            pluginconfigsent = true;
+            return bRC_More;
+         }
+      }
+
+      // check for next file only when no previous error
+      if (!openerror) {
+         if (estimate || objectsent) {
+            objectsent = false;
+            if (perform_read_metacommands(ctx) != bRC_OK) {
+               /* signal error */
+               return bRC_Error;
+            }
+         }
+
+         if (nextfile) {
+            DMSG1(ctx, DINFO, "nextfile %s backup!\n", fname.c_str());
+            return bRC_More;
+         }
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * The PLUGIN is using this callback to handle Core restore.
+   */
+   bRC PLUGINCLASS::startRestoreFile(bpContext *ctx, const char *cmd)
+   {
+      if (restoreobject_list.size() > 0) {
+         restore_object_class *ropclass;
+         POOL_MEM backcmd(PM_FNAME);
+
+         foreach_alist(ropclass, &restoreobject_list) {
+            if (!ropclass->sent && strcmp(cmd, ropclass->plugin_name.c_str()) == 0) {
+
+               Mmsg(backcmd, "RESTOREOBJ:%s\n", ropclass->object_name.c_str());
+               DMSG1(ctx, DINFO, "%s", backcmd.c_str());
+               ropclass->sent = true;
+
+               if (!backend.ctx->write_command(ctx, backcmd.c_str())) {
+                  DMSG0(ctx, DERROR, "Error sending RESTOREOBJ command\n");
+                  return bRC_Error;
+               }
+
+               Mmsg(backcmd, "RESTOREOBJ_LEN:%d\n", ropclass->length);
+               if (!backend.ctx->write_command(ctx, backcmd.c_str())) {
+                  DMSG0(ctx, DERROR, "Error sending RESTOREOBJ_LEN command\n");
+                  return bRC_Error;
+               }
+
+               /* send data */
+               if (backend.ctx->send_data(ctx, ropclass->data, ropclass->length) != bRC_OK) {
+                  DMSG0(ctx, DERROR, "Error sending RestoreObject data\n");
+                  return bRC_Error;
+               }
+            }
+         }
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * The PLUGIN is not using this callback to handle restore.
+   */
+   bRC PLUGINCLASS::endRestoreFile(bpContext *ctx)
+   {
+      return bRC_OK;
+   }
+
+   /*
+   * Prepares a file to restore attributes based on data from restore_pkt.
+   * It handles a response from backend to show if
+   *
+   * in:
+   *    bpContext - bacula plugin context
+   *    restore_pkt - Bacula Plugin API restore packet structure
+   * out:
+   *    bRC_OK - when success reported from backend
+   *    rp->create_status = CF_EXTRACT - the backend will restore the file
+   *                                     with pleasure
+   *    rp->create_status = CF_SKIP - the backend wants to skip restoration, i.e.
+   *                                  the file already exist and Replace=n was set
+   *    bRC_Error, rp->create_status = CF_ERROR - in any error
+   */
+   bRC PLUGINCLASS::createFile(bpContext *ctx, struct restore_pkt *rp)
+   {
+      POOL_MEM cmd(PM_FNAME);
+      // char type;
+
+      {
+         // synchronie access to job_cancelled variable
+         // smart_lock<smart_mutex> lg(&mutex); - removed on request
+         if (job_cancelled) {
+            return bRC_Error;
+         }
+      }
+
+      skipextract = false;
+      acldatalen = 0;
+      xattrdatalen = 0;
+      if (CORELOCALRESTORE && islocalpath(where)) {
+         DMSG0(ctx, DDEBUG, "createFile:Forwarding restore to Core\n");
+         rp->create_status = CF_CORE;
+      } else {
+         // FNAME:$fname$
+         Mmsg(cmd, "FNAME:%s\n", rp->ofname);
+         backend.ctx->write_command(ctx, cmd);
+         DMSG(ctx, DINFO, "createFile:%s", cmd.c_str());
+
+         // STAT:...
+         metaplugin::attributes::make_stat_command(ctx, cmd, rp);
+         backend.ctx->write_command(ctx, cmd);
+         last_type = rp->type;
+         DMSG(ctx, DINFO, "createFile:%s", cmd.c_str());
+
+         // TSTAMP:...
+         if (metaplugin::attributes::make_tstamp_command(ctx, cmd, rp) == metaplugin::attributes::Status_OK) {
+            backend.ctx->write_command(ctx, cmd);
+            DMSG(ctx, DINFO, "createFile:%s", cmd.c_str());
+         }
+
+         // LSTAT:$link$
+         if (rp->type == FT_LNK && rp->olname != NULL){
+            Mmsg(cmd, "LSTAT:%s\n", rp->olname);
+            backend.ctx->write_command(ctx, cmd);
+            DMSG(ctx, DINFO, "createFile:%s", cmd.c_str());
+         }
+
+         backend.ctx->signal_eod(ctx);
+
+         // check if backend accepted the file
+         if (backend.ctx->read_command(ctx, cmd) > 0){
+            DMSG(ctx, DINFO, "createFile:resp: %s\n", cmd.c_str());
+            if (strcmp(cmd.c_str(), "OK") == 0){
+               rp->create_status = CF_EXTRACT;
+            } else
+            if (strcmp(cmd.c_str(), "SKIP") == 0){
+               rp->create_status = CF_SKIP;
+               skipextract = true;
+            } else
+            if (strcmp(cmd.c_str(), "CORE") == 0){
+               rp->create_status = CF_CORE;
+            } else {
+               DMSG(ctx, DERROR, "Wrong backend response to create file, got: %s\n", cmd.c_str());
+               JMSG(ctx, backend.ctx->jmsg_err_level(), "Wrong backend response to create file, got: %s\n", cmd.c_str());
+               rp->create_status = CF_ERROR;
+               return bRC_Error;
+            }
+         } else {
+            if (backend.ctx->is_error()){
+               /* raise up error from backend */
+               rp->create_status = CF_ERROR;
+               return bRC_Error;
+            }
+         }
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * Unimplemented, always return bRC_OK.
+   */
+   bRC PLUGINCLASS::setFileAttributes(bpContext *ctx, struct restore_pkt *rp)
+   {
+      return bRC_OK;
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx
+    * @param exepath
+    * @return bRC
+    */
+   void PLUGINCLASS::setup_backend_command(bpContext *ctx, POOL_MEM &exepath)
+   {
+      DMSG(ctx, DINFO, "ExePath: %s\n", exepath.c_str());
+      Mmsg(backend_cmd, "%s/%s", exepath.c_str(), BACKEND_CMD);
+      DMSG(ctx, DINFO, "BackendPath: %s\n", backend_cmd.c_str());
+      if (access(backend_cmd.c_str(), X_OK) < 0)
+      {
+         berrno be;
+         DMSG2(ctx, DERROR, "Unable to use backend: %s Err=%s\n", backend_cmd.c_str(), be.bstrerror());
+         pm_strcpy(backend_error, be.bstrerror());
+         backend_available = false;
+      } else {
+         DMSG0(ctx, DINFO, "Backend available\n");
+         backend_available = true;
+      }
+   }
+
+   /**
+    * @brief
+    *
+    * @param ctx
+    * @param xacl
+    * @return bRC
+    */
+   bRC PLUGINCLASS::handleXACLdata(bpContext *ctx, struct xacl_pkt *xacl)
+   {
+      {
+         // synchronie access to job_cancelled variable
+         // smart_lock<smart_mutex> lg(&mutex); - removed on request
+         if (job_cancelled) {
+            return bRC_Error;
+         }
+      }
+
+      switch (xacl->func)
+      {
+      case BACL_BACKUP:
+         if (readacl) {
+            DMSG0(ctx, DINFO, "bacl_backup\n");
+            xacl->count = acldatalen;
+            xacl->content = acldata.c_str();
+            readacl= false;
+         } else {
+            xacl->count = 0;
+         }
+         break;
+      case BACL_RESTORE:
+         DMSG1(ctx, DINFO, "bacl_restore: %d\n", last_type);
+            if (!skipextract) {
+               if (last_type != FT_DIREND) {
+                  return perform_write_acl(ctx, xacl);
+               } else {
+                  DMSG0(ctx, DDEBUG, "delay ACL stream restore\n");
+                  acldatalen = xacl->count;
+                  pm_memcpy(acldata, xacl->content, acldatalen);
+               }
+            }
+            break;
+      case BXATTR_BACKUP:
+         if (readxattr){
+            DMSG0(ctx, DINFO, "bxattr_backup\n");
+            xacl->count = xattrdatalen;
+            xacl->content = xattrdata.c_str();
+            readxattr= false;
+         } else {
+            xacl->count = 0;
+         }
+         break;
+      case BXATTR_RESTORE:
+         DMSG1(ctx, DINFO, "bxattr_restore: %d\n", last_type);
+         if (!skipextract) {
+            if (last_type != FT_DIREND) {
+               return perform_write_xattr(ctx, xacl);
+            } else {
+               DMSG0(ctx, DDEBUG, "delay XATTR stream restore\n");
+               xattrdatalen = xacl->count;
+               pm_memcpy(xattrdata, xacl->content, xattrdatalen);
+            }
+         }
+         break;
+      }
+
+      return bRC_OK;
+   }
+
+   /*
+   * QueryParameter interface
+   */
+   bRC PLUGINCLASS::queryParameter(bpContext *ctx, struct query_pkt *qp)
+   {
+      DMSG0(ctx, D1, "PLUGINCLASS::queryParameter\n");
+
+      // check if it is our Plugin command
+      if (!isourplugincommand(PLUGINPREFIX, qp->command) != 0){
+         // it is not our plugin prefix
+         return bRC_OK;
+      }
+
+      {
+         // synchronie access to job_cancelled variable
+         // smart_lock<smart_mutex> lg(&mutex); - removed on request
+         if (job_cancelled) {
+            return bRC_Error;
+         }
+      }
+
+      POOL_MEM cmd(PM_MESSAGE);
+
+      if (listing == None) {
+         listing = Query;
+         Mmsg(cmd, "%s query=%s", qp->command, qp->parameter);
+         if (prepare_backend(ctx, BACKEND_JOB_INFO_ESTIMATE, cmd.c_str()) == bRC_Error){
+            return bRC_Error;
+         }
+      }
+
+      /* read backend response */
+      char pkt = 0;
+      int32_t pktlen = backend.ctx->read_any(ctx, &pkt, cmd);
+      if (pktlen < 0) {
+         DMSG(ctx, DERROR, "Cannot read backend query response for %s command.\n", qp->parameter);
+         JMSG(ctx, backend.ctx->jmsg_err_level(), "Cannot read backend query response for %s command.\n", qp->parameter);
+         return bRC_Error;
+      }
+
+      bRC ret = bRC_More;
+
+      /* check EOD */
+      if (backend.ctx->is_eod()){
+         /* got EOD so the backend finish response, so terminate the chat */
+         DMSG0(ctx, D1, "PLUGINCLASS::queryParameter: got EOD\n");
+         backend.ctx->signal_term(ctx);
+         backend.ctx->terminate(ctx);
+         qp->result = NULL;
+         ret = bRC_OK;
+      } else {
+         switch (pkt)
+         {
+         case 'C':
+            {
+               OutputWriter ow(qp->api_opts);
+               char *p, *q, *t;
+               alist values(10, not_owned_by_alist);
+               key_pair *kp;
+
+               /*
+               * here we have:
+               *    key=value[,key2=value2[,...]]
+               * parameters we should decompose
+               */
+               p = cmd.c_str();
+               while (*p != '\0') {
+                  q = strchr(p, ',');
+                  if (q != NULL) {
+                     *q++ = '\0';
+                  }
+                  // single key=value
+                  DMSG(ctx, D1, "PLUGINCLASS::queryParameter:scan %s\n", p);
+                  if ((t = strchr(p, '=')) != NULL) {
+                     *t++ = '\0';
+                  } else {
+                     t = (char*)"";     // pointer to empty string
+                  }
+                  DMSG2(ctx, D1, "PLUGINCLASS::queryParameter:pair '%s' = '%s'\n", p, t);
+                  if (strlen(p) > 0) {
+                     // push values only when we have key name
+                     kp = New(key_pair(p, t));
+                     values.append(kp);
+                  }
+                  p = q != NULL ? q : (char*)"";
+               }
+
+               // if more values then one then it is a list
+               if (values.size() > 1) {
+                  DMSG0(ctx, D1, "PLUGINCLASS::queryParameter: will render list\n")
+                  ow.start_list(qp->parameter);
+               }
+               // render all values
+               foreach_alist(kp, &values) {
+                  ow.get_output(OT_STRING, kp->key.c_str(), kp->value.c_str(), OT_END);
+                  delete kp;
+               }
+               if (values.size() > 1) {
+                  ow.end_list();
+               }
+               pm_strcpy(robjbuf, ow.get_output(OT_END));
+               qp->result = robjbuf.c_str();
+            }
+            break;
+         case 'D':
+            pm_memcpy(robjbuf, cmd.c_str(), pktlen);
+            qp->result = robjbuf.c_str();
+            break;
+         default:
+            DMSG(ctx, DERROR, "PLUGINCLASS::queryParameter: got invalid packet: %c\n", pkt);
+            JMSG(ctx, M_ERROR, "PLUGINCLASS::queryParameter: got invalid packet: %c\n", pkt);
+            backend.ctx->signal_term(ctx);
+            backend.ctx->terminate(ctx);
+            qp->result = NULL;
+            ret = bRC_Error;
+            break;
+         }
+      }
+
+      return ret;
+   }
+
+   /**
+    * @brief Sends metadata to backend for restore.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param mp
+    * @return bRC
+    */
+   bRC PLUGINCLASS::metadataRestore(bpContext *ctx, struct meta_pkt *mp)
+   {
+      {
+         // synchronie access to job_cancelled variable
+         // smart_lock<smart_mutex> lg(&mutex); - removed on request
+         if (job_cancelled) {
+            return bRC_Error;
+         }
+      }
+
+      if (!skipextract){
+         POOL_MEM cmd(PM_FNAME);
+
+         if (mp->buf != NULL && mp->buf_len > 0){
+            /* send command METADATA */
+            pm_strcpy(cmd, prepare_metadata_type(mp->type));
+            backend.ctx->write_command(ctx, cmd.c_str());
+            /* send metadata stream data */
+            DMSG1(ctx, DINFO, "writeMetadata: %i\n", mp->buf_len);
+            int rc = backend.ctx->write_data(ctx, (char*)mp->buf, mp->buf_len);
+            if (rc < 0){
+               /* got some error */
+               return bRC_Error;
+            }
+
+            // signal end of metadata stream to restore and get ack
+            backend.ctx->signal_eod(ctx);
+
+            // check if backend accepted the file
+            if (backend.ctx->read_command(ctx, cmd) > 0) {
+               DMSG(ctx, DINFO, "metadataRestore:resp: %s\n", cmd.c_str());
+               if (bstrcmp(cmd.c_str(), "SKIP")) {
+                  // SKIP!
+                  skipextract = true;
+                  return bRC_Skip;
+               }
+               if (!bstrcmp(cmd.c_str(), "OK")) {
+                  DMSG(ctx, DERROR, "Wrong backend response to metadataRestore, got: %s\n", cmd.c_str());
+                  JMSG(ctx, backend.ctx->jmsg_err_level(), "Wrong backend response to metadataRestore, got: %s\n", cmd.c_str());
+                  return bRC_Error;
+               }
+            } else {
+               if (backend.ctx->is_error()) {
+                  // raise up error from backend
+                  return bRC_Error;
+               }
+            }
+         }
+      }
+      return bRC_OK;
+   }
+
+   /**
+    * @brief Implements default metaplugin checkFile() callback.
+    *    When fname match plugin configured namespace then it return bRC_Seen by default
+    *    or calls custom checkFile() callback defined by backend developer.
+    *
+    * @param ctx for Bacula debug and jobinfo messages
+    * @param fname file name to check
+    * @return bRC bRC_Seen or bRC_OK
+    */
+   bRC PLUGINCLASS::checkFile(bpContext * ctx, char *fname)
+   {
+      if ((!CUSTOMNAMESPACE && isourpluginfname(PLUGINPREFIX, fname)) || (CUSTOMNAMESPACE && isourpluginfname(PLUGINNAMESPACE, fname)))
+      {
+         // synchronie access to job_cancelled variable
+         // smart_lock<smart_mutex> lg(&mutex); - removed on request
+         if (!job_cancelled) {
+            if (::checkFile != NULL) {
+               return ::checkFile(ctx, fname);
+            }
+         }
+         return bRC_Seen;
+      }
+
+      return bRC_OK;
+   }
+
+#endif
+
+}  // namespace pluginlib
diff --git a/bacula/src/plugins/fd/pluginlib/pluginctx.h b/bacula/src/plugins/fd/pluginlib/pluginctx.h
new file mode 100644 (file)
index 0000000..f1c2dfc
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2023 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.
+ */
+/**
+ * @file pluginctx.h
+ * @author Radosław Korzeniewski (radoslaw@korzeniewski.net)
+ * @brief This is a Bacula File Daemon general plugin framework. The Context.
+ * @version 1.0.0
+ * @date 2021-04-08
+ *
+ * @copyright Copyright (c) 2021 All rights reserved. IP transferred to Bacula Systems according to agreement.
+ */
+
+#include "pluginlib.h"
+#include "lib/bregex.h"
+
+#ifndef PLUGINLIB_PLUGINCTX_H
+#define PLUGINLIB_PLUGINCTX_H
+
+// The list of restore options saved to the RestoreObject.
+extern struct ini_items plugin_items_dump[];
+
+namespace pluginlib
+{
+   /*
+    * This is a main plugin API class. It manages a plugin context.
+    *  All the public methods correspond to a public Bacula API calls, even if
+    *  a callback is not implemented.
+    */
+   class PLUGINCTX : public SMARTALLOC
+   {
+   public:
+      PLUGINCTX(const char * command) :
+         cmd(PM_FNAME),
+         f_error(false),
+         f_fatal(false),
+         abort_on_error(false),
+         f_debug(false),
+         ini(),
+         preg()
+      { pm_strcpy(cmd, command); }
+#if __cplusplus > 201103L
+      PLUGINCTX() = delete;
+      PLUGINCTX(PLUGINCTX&) = delete;
+      PLUGINCTX(PLUGINCTX&&) = delete;
+#endif
+      virtual ~PLUGINCTX() {}
+
+      virtual bRC parse_parameter(bpContext *ctx, const char *argk, const char *argv) = 0;
+      virtual bRC parse_parameter(bpContext *ctx, ini_items &item) = 0;
+      virtual bRC parse_plugin_config(bpContext *ctx, restore_object_pkt *rop);
+      virtual bRC handle_restore_object(bpContext *ctx, restore_object_pkt *rop) { return bRC_OK; }
+
+      /**
+       * @brief Checks if plugin context operation is flagged on f_error.
+       *
+       * @return true when is error
+       * @return false when no error
+       */
+      inline bool is_error() { return f_error || f_fatal; }
+
+      /**
+       * @brief Checks if plugin context operation is flagged on f_fatal.
+       *
+       * @return true when is fatal error
+       * @return false when no fatal error
+       */
+      inline bool is_fatal() { return f_fatal || (f_error && abort_on_error); }
+
+      /**
+       * @brief Return a Job Message error level based on context
+       *
+       * @return int
+       */
+      inline int jmsg_err_level() { return is_fatal() ? M_FATAL : M_ERROR; }
+
+      /**
+       * @brief Set the abort on error flag
+       */
+      inline void set_abort_on_error() { abort_on_error = true; }
+
+      /**
+       * @brief Clears the abort on error flag.
+       */
+      inline void clear_abort_on_error() { abort_on_error = false; }
+
+      /**
+       * @brief return abort on error flag status
+       *
+       * @return true if flag is set
+       * @return false  if flag is not set
+       */
+      bool is_abort_on_error() { return abort_on_error; }
+
+   protected:
+      POOL_MEM cmd;                 /// plugin command for this context
+      bool f_error;                 /// the plugin signaled an error
+      bool f_fatal;                 /// the plugin signaled a fatal error
+      bool abort_on_error;          /// abort on error flag
+      bool f_debug;                 /// when additional debugging required
+      ConfigFile ini;               /// Restore ini file handler
+      regex_t preg;                 /// this is a regex context for include/exclude
+
+      virtual int check_ini_param(char *param);
+      virtual bool check_plugin_param(const char *param, alist *params) { return false; }
+      virtual int get_ini_count() { return 0; }
+   };
+}  // namespace pluginlib
+
+#endif   // PLUGINLIB_PLUGINBASE_H