]> git.sur5r.net Git - bacula/bacula/blobdiff - bacula/src/dird/restore.c
Apply patch from Wandlei Huttel to add Run Time and suffix to Restored bytes
[bacula/bacula] / bacula / src / dird / restore.c
index b75b82935de1e75109a8ec7cc34b9884dc57801b..e3d7f8c2d681d9585d9b4972a8e1d397167f395f 100644 (file)
@@ -1,34 +1,25 @@
 /*
-   Bacula® - The Network Backup Solution
-
-   Copyright (C) 2000-2010 Free Software Foundation Europe e.V.
-
-   The main author of Bacula is Kern Sibbald, with contributions from
-   many others, a complete list can be found in the file AUTHORS.
-   This program is Free Software; you can redistribute it and/or
-   modify it under the terms of version two of the GNU General Public
-   License as published by the Free Software Foundation and included
-   in the file LICENSE.
-
-   This program is distributed in the hope that it will be useful, but
-   WITHOUT ANY WARRANTY; without even the implied warranty of
-   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-   General Public License for more details.
-
-   You should have received a copy of the GNU General Public License
-   along with this program; if not, write to the Free Software
-   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-   02110-1301, USA.
-
-   Bacula® is a registered trademark of Kern Sibbald.
-   The licensor of Bacula is the Free Software Foundation Europe
-   (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
-   Switzerland, email:ftf@fsfeurope.org.
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2015 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.
 */
 /**
  *   Bacula Director -- restore.c -- responsible for restoring files
  *
- *     Kern Sibbald, November MM
+ *     Written by Kern Sibbald, November MM
  *
  *    This routine is run as a separate thread.
  *
 
 #include "bacula.h"
 #include "dird.h"
+#include "lib/ini.h"
 
 /* Commands sent to File daemon */
-static char restorecmd[]  = "restore replace=%c prelinks=%d where=%s\n";
-static char restorecmdR[] = "restore replace=%c prelinks=%d regexwhere=%s\n";
-static char storaddr[]    = "storage address=%s port=%d ssl=0 Authorization=%s\n";
+static char restorecmd[]  = "restore %sreplace=%c prelinks=%d where=%s\n";
+static char restorecmdR[] = "restore %sreplace=%c prelinks=%d regexwhere=%s\n";
+static char storaddr[]    = "storage address=%s port=%d ssl=%d Authorization=%s\n";
 
 /* Responses received from File daemon */
 static char OKrestore[]   = "2000 OK restore\n";
@@ -65,6 +57,7 @@ static void build_restore_command(JCR *jcr, POOL_MEM &ret)
 {
    char replace, *where, *cmd;
    char empty = '\0';
+   char files[100];
 
    /* Build the restore command */
 
@@ -75,7 +68,7 @@ static void build_restore_command(JCR *jcr, POOL_MEM &ret)
    } else {
       replace = REPLACE_ALWAYS;       /* always replace */
    }
-   
+
    if (jcr->RegexWhere) {
       where = jcr->RegexWhere;             /* override */
       cmd = restorecmdR;
@@ -92,13 +85,18 @@ static void build_restore_command(JCR *jcr, POOL_MEM &ret)
 
    } else {                           /* nothing was specified */
       where = ∅                 /* use default */
-      cmd   = restorecmd;                    
+      cmd   = restorecmd;
    }
-   
+
    jcr->prefix_links = jcr->job->PrefixLinks;
 
    bash_spaces(where);
-   Mmsg(ret, cmd, replace, jcr->prefix_links, where);
+   if (jcr->FDVersion < 7) {
+      Mmsg(ret, cmd, "", replace, jcr->prefix_links, where);
+   } else {
+      snprintf(files, sizeof(files), "files=%d ", jcr->ExpectedFiles);
+      Mmsg(ret, cmd, files, replace, jcr->prefix_links, where);
+   }
    unbash_spaces(where);
 }
 
@@ -111,15 +109,15 @@ struct bootstrap_info
 
 #define UA_CMD_SIZE 1000
 
-/*
- * Open the bootstrap file and find the first Storage= 
+/**
+ * Open the bootstrap file and find the first Storage=
  * Returns ok if able to open
- * It fills the storage name (should be the first line) 
- * and the file descriptor to the bootstrap file, 
+ * It fills the storage name (should be the first line)
+ * and the file descriptor to the bootstrap file,
  * it should be used for next operations, and need to be closed
  * at the end.
  */
-static bool open_bootstrap_file(JCR *jcr, struct bootstrap_info &info)
+static bool open_bootstrap_file(JCR *jcr, bootstrap_info &info)
 {
    FILE *bs;
    UAContext *ua;
@@ -131,12 +129,12 @@ static bool open_bootstrap_file(JCR *jcr, struct bootstrap_info &info)
    }
    strncpy(info.storage, jcr->rstore->name(), MAX_NAME_LENGTH);
 
-   bs = fopen(jcr->RestoreBootstrap, "rb");
+   bs = bfopen(jcr->RestoreBootstrap, "rb");
    if (!bs) {
       berrno be;
       Jmsg(jcr, M_FATAL, 0, _("Could not open bootstrap file %s: ERR=%s\n"),
          jcr->RestoreBootstrap, be.bstrerror());
-      set_jcr_job_status(jcr, JS_ErrorTerminated);
+      jcr->setJobStatus(JS_ErrorTerminated);
       return false;
    }
 
@@ -158,7 +156,7 @@ static bool open_bootstrap_file(JCR *jcr, struct bootstrap_info &info)
    return true;
 }
 
-/** 
+/**
  * This function compare the given storage name with the
  * the current one. We compare the name and the address:port.
  * Returns true if we use the same storage.
@@ -171,7 +169,7 @@ static bool is_on_same_storage(JCR *jcr, char *new_one)
    if (jcr->FDVersion < 2) {
       return true;
    }
-   /* we are in init loop ? shoudn't fall here */
+   /* we are in init loop ? shoudn't fail here */
    if (!*new_one) {
       return true;
    }
@@ -181,10 +179,10 @@ static bool is_on_same_storage(JCR *jcr, char *new_one)
    }
    new_store = (STORE *)GetResWithName(R_STORAGE, new_one);
    if (!new_store) {
-      Jmsg(jcr, M_FATAL, 0,
+      Jmsg(jcr, M_WARNING, 0,
            _("Could not get storage resource '%s'.\n"), new_one);
-      set_jcr_job_status(jcr, JS_ErrorTerminated);
-      return false;
+      /* If not storage found, use last one */
+      return true;
    }
    /* if Port and Hostname/IP are same, we are talking to the same
     * Storage Daemon
@@ -197,15 +195,15 @@ static bool is_on_same_storage(JCR *jcr, char *new_one)
    return true;
 }
 
-/** 
+/**
  * Check if the current line contains Storage="xxx", and compare the
- * result to the current storage. We use UAContext to analyse the bsr 
+ * result to the current storage. We use UAContext to analyse the bsr
  * string.
  *
  * Returns true if we need to change the storage, and it set the new
- * Storage resource name in "storage" arg. 
+ * Storage resource name in "storage" arg.
  */
-static bool check_for_new_storage(JCR *jcr, struct bootstrap_info &info)
+static bool check_for_new_storage(JCR *jcr, bootstrap_info &info)
 {
    UAContext *ua = info.ua;
    parse_ua_args(ua);
@@ -229,7 +227,7 @@ static bool check_for_new_storage(JCR *jcr, struct bootstrap_info &info)
  * Send bootstrap file to Storage daemon section by section.
  */
 static bool send_bootstrap_file(JCR *jcr, BSOCK *sock,
-                                struct bootstrap_info &info)
+                                bootstrap_info &info)
 {
    boffset_t pos;
    const char *bootstrap = "bootstrap\n";
@@ -245,7 +243,7 @@ static bool send_bootstrap_file(JCR *jcr, BSOCK *sock,
    while(fgets(ua->cmd, UA_CMD_SIZE, bs)) {
       if (check_for_new_storage(jcr, info)) {
          /* Otherwise, we need to contact another storage daemon.
-          * Reset bs to the beginning of the current segment. 
+          * Reset bs to the beginning of the current segment.
           */
          fseeko(bs, pos, SEEK_SET);
          break;
@@ -257,37 +255,67 @@ static bool send_bootstrap_file(JCR *jcr, BSOCK *sock,
    return true;
 }
 
-/** 
+#define MAX_TRIES 6 * 360   /* 6 hours */
+
+/**
  * Change the read storage resource for the current job.
  */
-static void select_rstore(JCR *jcr, struct bootstrap_info &info)
+static bool select_rstore(JCR *jcr, bootstrap_info &info)
 {
    USTORE ustore;
+   int i;
+
 
    if (!strcmp(jcr->rstore->name(), info.storage)) {
-      return;
+      return true;                 /* same SD nothing to change */
    }
 
    if (!(ustore.store = (STORE *)GetResWithName(R_STORAGE,info.storage))) {
       Jmsg(jcr, M_FATAL, 0,
            _("Could not get storage resource '%s'.\n"), info.storage);
-      set_jcr_job_status(jcr, JS_ErrorTerminated);
-      return;
-   }
-   
-   if (jcr->store_bsock) {
-      jcr->store_bsock->destroy();
-      jcr->store_bsock = NULL;
+      jcr->setJobStatus(JS_ErrorTerminated);
+      return false;
    }
-   
+
+   /*
+    * This releases the store_bsock between calls to the SD.
+    *  I think.
+    */
+   free_bsock(jcr->store_bsock);
+
+   /*
+    * release current read storage and get a new one
+    */
+   dec_read_store(jcr);
    free_rstorage(jcr);
    set_rstorage(jcr, &ustore);
+   jcr->setJobStatus(JS_WaitSD);
+   /*
+    * Wait for up to 6 hours to increment read stoage counter
+    */
+   for (i=0; i < MAX_TRIES; i++) {
+      /* try to get read storage counter incremented */
+      if (inc_read_store(jcr)) {
+         jcr->setJobStatus(JS_Running);
+         return true;
+      }
+      bmicrosleep(10, 0);       /* sleep 10 secs */
+      if (job_canceled(jcr)) {
+         free_rstorage(jcr);
+         return false;
+      }
+   }
+   /* Failed to inc_read_store() */
+   free_rstorage(jcr);
+   Jmsg(jcr, M_FATAL, 0,
+      _("Could not acquire read storage lock for \"%s\""), info.storage);
+   return false;
 }
 
-/* 
- * Clean the struct bootstrap_info struct
+/*
+ * Clean the bootstrap_info struct
  */
-static void close_bootstrap_file(struct bootstrap_info &info)
+static void close_bootstrap_file(bootstrap_info &info)
 {
    if (info.bs) {
       fclose(info.bs);
@@ -297,7 +325,7 @@ static void close_bootstrap_file(struct bootstrap_info &info)
    }
 }
 
-/** 
+/**
  * The bootstrap is stored in a file, so open the file, and loop
  *   through it processing each storage device in turn. If the
  *   storage is different from the prior one, we open a new connection
@@ -308,24 +336,27 @@ static void close_bootstrap_file(struct bootstrap_info &info)
  */
 bool restore_bootstrap(JCR *jcr)
 {
-   BSOCK *fd=NULL, *sd;
-   bool end_loop=false;
-   bool first_time=true;
-   struct bootstrap_info info;
+   int tls_need = BNET_TLS_NONE;
+   BSOCK *fd = NULL;
+   BSOCK *sd;
+   char *store_address;
+   uint32_t store_port;
+   bool first_time = true;
+   bootstrap_info info;
    POOL_MEM restore_cmd(PM_MESSAGE);
-   bool ret=false;
+   bool ret = false;
+
 
-   /* this command is used for each part */
-   build_restore_command(jcr, restore_cmd);
-   
    /* Open the bootstrap file */
    if (!open_bootstrap_file(jcr, info)) {
       goto bail_out;
    }
    /* Read the bootstrap file */
-   while (!end_loop && !feof(info.bs)) {
-      
-      select_rstore(jcr, info);
+   while (!feof(info.bs)) {
+
+      if (!select_rstore(jcr, info)) {
+         goto bail_out;
+      }
 
       /**
        * Open a message channel connection with the Storage
@@ -334,7 +365,7 @@ bool restore_bootstrap(JCR *jcr)
        *
        */
       Dmsg0(10, "Open connection with storage daemon\n");
-      set_jcr_job_status(jcr, JS_WaitSD);
+      jcr->setJobStatus(JS_WaitSD);
       /*
        * Start conversation with Storage daemon
        */
@@ -353,15 +384,16 @@ bool restore_bootstrap(JCR *jcr)
          /*
           * Start conversation with File daemon
           */
-         set_jcr_job_status(jcr, JS_WaitFD);
+         jcr->setJobStatus(JS_WaitFD);
          jcr->keep_sd_auth_key = true; /* don't clear the sd_auth_key now */
          if (!connect_to_file_daemon(jcr, 10, FDConnectTimeout, 1)) {
             goto bail_out;
          }
          fd = jcr->file_bsock;
+         build_restore_command(jcr, restore_cmd);
       }
 
-      set_jcr_job_status(jcr, JS_WaitSD);
+      jcr->setJobStatus(JS_Running);
 
       /*
        * Send the bootstrap file -- what Volumes/files to restore
@@ -371,54 +403,91 @@ bool restore_bootstrap(JCR *jcr)
          goto bail_out;
       }
 
-      if (!sd->fsend("run")) {
-         goto bail_out;
+      if (jcr->sd_calls_client) {
+         /*
+          * SD must call "client" i.e. FD
+          */
+         if (jcr->FDVersion < 10) {
+            Jmsg(jcr, M_FATAL, 0, _("The File daemon does not support SDCallsClient.\n"));
+            goto bail_out;
+         }
+         if (!send_client_addr_to_sd(jcr)) {
+            goto bail_out;
+         }
+         if (!run_storage_and_start_message_thread(jcr, sd)) {
+            goto bail_out;
+         }
+
+         store_address = jcr->rstore->address;  /* dummy */
+         store_port = 0;                        /* flag that SD calls FD */
+
+      } else {
+         /*
+          * Default case where FD must call the SD
+          */
+         if (!run_storage_and_start_message_thread(jcr, sd)) {
+            goto bail_out;
+         }
+
+         /*
+          * send Storage daemon address to the File daemon,
+          *   then wait for File daemon to make connection
+          *   with Storage daemon.
+          */
+         if (jcr->rstore->SDDport == 0) {
+            jcr->rstore->SDDport = jcr->rstore->SDport;
+         }
+
+         store_address = get_storage_address(jcr->client, jcr->rstore);
+         store_port = jcr->rstore->SDDport;
       }
-      /*
-       * Now start a Storage daemon message thread
-       */
-      if (!start_storage_daemon_message_thread(jcr)) {
-         goto bail_out;
+
+      /* TLS Requirement */
+      if (jcr->rstore->tls_enable) {
+         if (jcr->rstore->tls_require) {
+            tls_need = BNET_TLS_REQUIRED;
+         } else {
+            tls_need = BNET_TLS_OK;
+         }
       }
-      Dmsg0(50, "Storage daemon connection OK\n");
 
       /*
-       * send Storage daemon address to the File daemon,
-       *   then wait for File daemon to make connection
-       *   with Storage daemon.
+       * Send storage address to FD
+       *  if port==0 FD must wait for SD to call it.
        */
-      if (jcr->rstore->SDDport == 0) {
-         jcr->rstore->SDDport = jcr->rstore->SDport;
-      }
-      fd->fsend(storaddr, jcr->rstore->address, jcr->rstore->SDDport,
-                jcr->sd_auth_key);
+      fd->fsend(storaddr, store_address, store_port, tls_need, jcr->sd_auth_key);
       memset(jcr->sd_auth_key, 0, strlen(jcr->sd_auth_key));
-
       Dmsg1(6, "dird>filed: %s\n", fd->msg);
       if (!response(jcr, fd, OKstore, "Storage", DISPLAY_ERROR)) {
          goto bail_out;
       }
 
+      /* Declare the job started to start the MaxRunTime check */
+      jcr->setJobStarted();
+
+      /* Only pass "global" commands to the FD once */
       if (first_time) {
+         first_time = false;
          if (!send_runscripts_commands(jcr)) {
             goto bail_out;
          }
-         first_time=false;
-      }
-
-      if (!send_restore_objects(jcr)) {
-         goto bail_out;
+         if (!send_component_info(jcr)) {
+            Pmsg0(000, "FAIL: Send component info\n");
+            goto bail_out;
+         }
+         if (!send_restore_objects(jcr)) {
+            Pmsg0(000, "FAIL: Send restore objects\n");
+            goto bail_out;
+         }
       }
 
       fd->fsend("%s", restore_cmd.c_str());
-
       if (!response(jcr, fd, OKrestore, "Restore", DISPLAY_ERROR)) {
          goto bail_out;
       }
 
       if (jcr->FDVersion < 2) { /* Old FD */
-         end_loop=true;         /* we do only one loop */
-
+         break;                 /* we do only one loop */
       } else {
          if (!response(jcr, fd, OKstoreend, "Store end", DISPLAY_ERROR)) {
             goto bail_out;
@@ -476,6 +545,10 @@ bool do_restore(JCR *jcr)
    /* Print Job Start message */
    Jmsg(jcr, M_INFO, 0, _("Start Restore Job %s\n"), jcr->Job);
 
+   if (jcr->client) {
+      jcr->sd_calls_client = jcr->client->sd_calls_client;
+   }
+
    /* Read the bootstrap file and do the restore */
    if (!restore_bootstrap(jcr)) {
       goto bail_out;
@@ -491,8 +564,40 @@ bail_out:
    return false;
 }
 
-bool do_restore_init(JCR *jcr) 
+/* Create a Plugin Config RestoreObject, will be sent
+ * at restore time to the Plugin
+ */
+static void plugin_create_restoreobject(JCR *jcr, plugin_config_item *elt)
+{
+   ROBJECT_DBR ro;
+   memset(&ro, 0, sizeof(ro));
+   ro.FileIndex = 1;
+   ro.JobId = jcr->JobId;
+   ro.FileType = FT_PLUGIN_CONFIG_FILLED;
+   ro.object_index = 1;
+   ro.object_full_len = ro.object_len = strlen(elt->content);
+   ro.object_compression = 0;
+   ro.plugin_name = elt->plugin_name;
+   ro.object_name = (char*)INI_RESTORE_OBJECT_NAME;
+   ro.object = elt->content;
+   db_create_restore_object_record(jcr, jcr->db, &ro);
+   Dmsg1(50, "Creating restore object for %s\n", elt->plugin_name);
+}
+
+bool do_restore_init(JCR *jcr)
 {
+   /* Will add RestoreObject used for the Plugin configuration */
+   if (jcr->plugin_config) {
+
+      plugin_config_item *elt;
+      foreach_alist(elt, jcr->plugin_config) {
+         plugin_create_restoreobject(jcr, elt);
+         free_plugin_config_item(elt);
+      }
+
+      delete jcr->plugin_config;
+      jcr->plugin_config = NULL;
+   }
    free_wstorage(jcr);
    return true;
 }
@@ -504,15 +609,25 @@ bool do_restore_init(JCR *jcr)
 void restore_cleanup(JCR *jcr, int TermCode)
 {
    char sdt[MAX_TIME_LENGTH], edt[MAX_TIME_LENGTH];
-   char ec1[30], ec2[30], ec3[30];
+   char ec1[30], ec2[30], ec3[30], ec4[30], elapsed[50];
    char term_code[100], fd_term_msg[100], sd_term_msg[100];
    const char *term_msg;
    int msg_type = M_INFO;
    double kbps;
+   utime_t RunTime;
 
    Dmsg0(20, "In restore_cleanup\n");
    update_job_end(jcr, TermCode);
 
+   if (jcr->component_fd) {
+      fclose(jcr->component_fd);
+      jcr->component_fd = NULL;
+   }
+   if (jcr->component_fname && *jcr->component_fname) {
+      unlink(jcr->component_fname);
+   }
+   free_and_null_pool_memory(jcr->component_fname);
+
    if (jcr->unlink_bsr && jcr->RestoreBootstrap) {
       unlink(jcr->RestoreBootstrap);
       jcr->unlink_bsr = false;
@@ -520,12 +635,16 @@ void restore_cleanup(JCR *jcr, int TermCode)
 
    if (job_canceled(jcr)) {
       cancel_storage_daemon_job(jcr);
-   }  
+   }
 
    switch (TermCode) {
    case JS_Terminated:
       if (jcr->ExpectedFiles > jcr->jr.JobFiles) {
          term_msg = _("Restore OK -- warning file count mismatch");
+
+      } else if (jcr->JobErrors > 0 || jcr->SDErrors > 0) {
+         term_msg = _("Restore OK -- with errors");
+
       } else {
          term_msg = _("Restore OK");
       }
@@ -539,7 +658,7 @@ void restore_cleanup(JCR *jcr, int TermCode)
       msg_type = M_ERROR;          /* Generate error message */
       if (jcr->store_bsock) {
          jcr->store_bsock->signal(BNET_TERMINATE);
-         if (jcr->SD_msg_chan) {
+         if (jcr->SD_msg_chan_started) {
             pthread_cancel(jcr->SD_msg_chan);
          }
       }
@@ -548,7 +667,7 @@ void restore_cleanup(JCR *jcr, int TermCode)
       term_msg = _("Restore Canceled");
       if (jcr->store_bsock) {
          jcr->store_bsock->signal(BNET_TERMINATE);
-         if (jcr->SD_msg_chan) {
+         if (jcr->SD_msg_chan_started) {
             pthread_cancel(jcr->SD_msg_chan);
          }
       }
@@ -560,11 +679,12 @@ void restore_cleanup(JCR *jcr, int TermCode)
    }
    bstrftimes(sdt, sizeof(sdt), jcr->jr.StartTime);
    bstrftimes(edt, sizeof(edt), jcr->jr.EndTime);
-   if (jcr->jr.EndTime - jcr->jr.StartTime > 0) {
-      kbps = (double)jcr->jr.JobBytes / (1000 * (jcr->jr.EndTime - jcr->jr.StartTime));
-   } else {
-      kbps = 0;
+
+   RunTime = jcr->jr.EndTime - jcr->jr.StartTime;
+   if (RunTime <= 0) {
+      RunTime = 1;
    }
+   kbps = (double)jcr->jr.JobBytes / (1000.0 * (double)RunTime);
    if (kbps < 0.05) {
       kbps = 0;
    }
@@ -572,31 +692,33 @@ void restore_cleanup(JCR *jcr, int TermCode)
    jobstatus_to_ascii(jcr->FDJobStatus, fd_term_msg, sizeof(fd_term_msg));
    jobstatus_to_ascii(jcr->SDJobStatus, sd_term_msg, sizeof(sd_term_msg));
 
-   Jmsg(jcr, msg_type, 0, _("%s %s %s (%s): %s\n"
+   Jmsg(jcr, msg_type, 0, _("%s %s %s (%s):\n"
 "  Build OS:               %s %s %s\n"
 "  JobId:                  %d\n"
 "  Job:                    %s\n"
 "  Restore Client:         %s\n"
 "  Start time:             %s\n"
 "  End time:               %s\n"
+"  Elapsed time:           %s\n"
 "  Files Expected:         %s\n"
 "  Files Restored:         %s\n"
-"  Bytes Restored:         %s\n"
+"  Bytes Restored:         %s (%sB)\n"
 "  Rate:                   %.1f KB/s\n"
 "  FD Errors:              %d\n"
 "  FD termination status:  %s\n"
 "  SD termination status:  %s\n"
 "  Termination:            %s\n\n"),
-        BACULA, my_name, VERSION, LSMDATE, edt,
+        BACULA, my_name, VERSION, LSMDATE,
         HOST_OS, DISTNAME, DISTVER,
         jcr->jr.JobId,
         jcr->jr.Job,
         jcr->client->name(),
         sdt,
         edt,
+        edit_utime(RunTime, elapsed, sizeof(elapsed)),
         edit_uint64_with_commas((uint64_t)jcr->ExpectedFiles, ec1),
         edit_uint64_with_commas((uint64_t)jcr->jr.JobFiles, ec2),
-        edit_uint64_with_commas(jcr->jr.JobBytes, ec3),
+        edit_uint64_with_commas(jcr->jr.JobBytes, ec3), edit_uint64_with_suffix(jcr->jr.JobBytes, ec4),
         (float)kbps,
         jcr->JobErrors,
         fd_term_msg,