]> git.sur5r.net Git - bacula/bacula/blobdiff - bacula/src/dird/ua_restore.c
Final fix of bug #1943
[bacula/bacula] / bacula / src / dird / ua_restore.c
index f41db51da68c6dfab7b7a4da758459abe3dafb17..9a9b44d60338e698a21aacf1b2e6df98d5a19358 100644 (file)
 /*
- *
- *   Bacula Director -- User Agent Database restore Command
- *     Creates a bootstrap file for restoring files
- *
- *     Kern Sibbald, July MMII
- *
- *   Version $Id$
- */
+   Bacula® - The Network Backup Solution
 
-/*
-   Copyright (C) 2002-2003 Kern Sibbald and John Walker
+   Copyright (C) 2002-2011 Free Software Foundation Europe e.V.
 
-   This program is free software; you can redistribute it and/or
-   modify it under the terms of the GNU General Public License as
-   published by the Free Software Foundation; either version 2 of
-   the License, or (at your option) any later version.
+   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 three of the GNU Affero 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
+   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., 59 Temple Place - Suite 330, Boston,
-   MA 02111-1307, USA.
+   You should have received a copy of the GNU Affero 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 Director -- User Agent Database restore Command
+ *      Creates a bootstrap file for restoring files and
+ *      starts the restore job.
+ *
+ *      Tree handling routines split into ua_tree.c July MMIII.
+ *      BSR (bootstrap record) handling routines split into
+ *        bsr.c July MMIII
+ *
+ *     Kern Sibbald, July MMII
  */
 
+
 #include "bacula.h"
 #include "dird.h"
-#include <fnmatch.h>
-#include "findlib/find.h"
-
-
 
 /* Imported functions */
-extern int runcmd(UAContext *ua, char *cmd);
-
-/* Imported variables */
-extern char *uar_list_jobs,    *uar_file,        *uar_sel_files;
-extern char *uar_del_temp,     *uar_del_temp1,   *uar_create_temp;
-extern char *uar_create_temp1, *uar_last_full,   *uar_full;
-extern char *uar_inc,          *uar_list_temp,   *uar_sel_jobid_temp;
-extern char *uar_sel_all_temp1, *uar_sel_fileset, *uar_mediatype;
-
-
-/* Context for insert_tree_handler() */
-typedef struct s_tree_ctx {
-   TREE_ROOT *root;                  /* root */
-   TREE_NODE *node;                  /* current node */
-   TREE_NODE *avail_node;            /* unused node last insert */
-   int cnt;                          /* count for user feedback */
-   UAContext *ua;
-} TREE_CTX;
-
-/* Main structure for obtaining JobIds */
-typedef struct s_jobids {
-   utime_t JobTDate;
-   uint32_t TotalFiles;
-   char ClientName[MAX_NAME_LENGTH];
-   char JobIds[200];
-   STORE  *store;
-} JobIds;
-
-
-/* FileIndex entry in bootstrap record */
-typedef struct s_rbsr_findex {
-   struct s_rbsr_findex *next;
-   int32_t findex;
-   int32_t findex2;
-} RBSR_FINDEX;
-
-/* Restore bootstrap record -- not the real one, but useful here */
-typedef struct s_rbsr {
-   struct s_rbsr *next;              /* next JobId */
-   uint32_t JobId;                   /* JobId this bsr */
-   uint32_t VolSessionId;                  
-   uint32_t VolSessionTime;
-   int     VolCount;                 /* Volume parameter count */
-   VOL_PARAMS *VolParams;            /* Volume, start/end file/blocks */
-   RBSR_FINDEX *fi;                  /* File indexes this JobId */
-} RBSR;
-
-typedef struct s_name_ctx {
-   char **name;                      /* list of names */
-   int num_ids;                      /* ids stored */
-   int max_ids;                      /* size of array */
-   int num_del;                      /* number deleted */
-   int tot_ids;                      /* total to process */
-} NAME_LIST;
-
-#define MAX_ID_LIST_LEN 1000000
+extern void print_bsr(UAContext *ua, RBSR *bsr);
 
 
 /* Forward referenced functions */
-static RBSR *new_bsr();
-static void free_bsr(RBSR *bsr);
-static void write_bsr(UAContext *ua, RBSR *bsr, FILE *fd);
-static int  write_bsr_file(UAContext *ua, RBSR *bsr);
-static void print_bsr(UAContext *ua, RBSR *bsr);
-static int  complete_bsr(UAContext *ua, RBSR *bsr);
-static int insert_tree_handler(void *ctx, int num_fields, char **row);
-static void add_findex(RBSR *bsr, uint32_t JobId, int32_t findex);
 static int last_full_handler(void *ctx, int num_fields, char **row);
 static int jobid_handler(void *ctx, int num_fields, char **row);
-static int next_jobid_from_list(char **p, uint32_t *JobId);
-static int user_select_jobids(UAContext *ua, JobIds *ji);
-static void user_select_files(TREE_CTX *tree);
+static int user_select_jobids_or_files(UAContext *ua, RESTORE_CTX *rx);
 static int fileset_handler(void *ctx, int num_fields, char **row);
-static void print_name_list(UAContext *ua, NAME_LIST *name_list);
-static int unique_name_list_handler(void *ctx, int num_fields, char **row);
 static void free_name_list(NAME_LIST *name_list);
-static void get_storage_from_mediatype(UAContext *ua, NAME_LIST *name_list, JobIds *ji);
-
+static bool select_backups_before_date(UAContext *ua, RESTORE_CTX *rx, char *date);
+static bool build_directory_tree(UAContext *ua, RESTORE_CTX *rx);
+static void free_rx(RESTORE_CTX *rx);
+static void split_path_and_filename(UAContext *ua, RESTORE_CTX *rx, char *fname);
+static int jobid_fileindex_handler(void *ctx, int num_fields, char **row);
+static bool insert_file_into_findex_list(UAContext *ua, RESTORE_CTX *rx, char *file,
+                                         char *date);
+static bool insert_dir_into_findex_list(UAContext *ua, RESTORE_CTX *rx, char *dir,
+                                        char *date);
+static void insert_one_file_or_dir(UAContext *ua, RESTORE_CTX *rx, char *date, bool dir);
+static int get_client_name(UAContext *ua, RESTORE_CTX *rx);
+static int get_restore_client_name(UAContext *ua, RESTORE_CTX &rx);
+static bool get_date(UAContext *ua, char *date, int date_len);
+static int restore_count_handler(void *ctx, int num_fields, char **row);
+static bool insert_table_into_findex_list(UAContext *ua, RESTORE_CTX *rx, char *table);
+static void get_and_display_basejobs(UAContext *ua, RESTORE_CTX *rx);
 
 /*
  *   Restore files
  *
  */
-int restorecmd(UAContext *ua, char *cmd)
+int restore_cmd(UAContext *ua, const char *cmd)
 {
-   POOLMEM *query;
-   TREE_CTX tree;
-   JobId_t JobId, last_JobId;
-   char *p;
-   RBSR *bsr;
-   char *nofname = "";
-   JobIds ji;
-   JOB *job = NULL;
-   JOB *restore_job = NULL;
-   int restore_jobs = 0;
-   NAME_LIST name_list;
-   uint32_t selected_files = 0;
-
-   if (!open_db(ua)) {
-      return 0;
+   RESTORE_CTX rx;                    /* restore context */
+   POOL_MEM buf;
+   JOB *job;
+   int i;
+   JCR *jcr = ua->jcr;
+   char *escaped_bsr_name = NULL;
+   char *escaped_where_name = NULL;
+   char *strip_prefix, *add_prefix, *add_suffix, *regexp;
+   strip_prefix = add_prefix = add_suffix = regexp = NULL;
+
+   memset(&rx, 0, sizeof(rx));
+   rx.path = get_pool_memory(PM_FNAME);
+   rx.fname = get_pool_memory(PM_FNAME);
+   rx.JobIds = get_pool_memory(PM_FNAME);
+   rx.JobIds[0] = 0;
+   rx.BaseJobIds = get_pool_memory(PM_FNAME);
+   rx.query = get_pool_memory(PM_FNAME);
+   rx.bsr = new_bsr();
+
+   i = find_arg_with_value(ua, "comment");
+   if (i >= 0) {
+      rx.comment = ua->argv[i];
+      if (!is_comment_legal(ua, rx.comment)) {
+         goto bail_out;
+      }
    }
 
-   memset(&tree, 0, sizeof(TREE_CTX));
-   memset(&name_list, 0, sizeof(name_list));
-   memset(&ji, 0, sizeof(ji));
+   i = find_arg_with_value(ua, "where");
+   if (i >= 0) {
+      rx.where = ua->argv[i];
+   }
 
-   /* Ensure there is at least one Restore Job */
-   LockRes();
-   while ( (job = (JOB *)GetNextRes(R_JOB, (RES *)job)) ) {
-      if (job->JobType == JT_RESTORE) {
-        if (!restore_job) {
-           restore_job = job;
-        }
-        restore_jobs++;
-      }
+   i = find_arg_with_value(ua, "replace");
+   if (i >= 0) {
+      rx.replace = ua->argv[i];
    }
-   UnlockRes();
-   if (!restore_jobs) {
-      bsendmsg(ua, _(
-         "No Restore Job Resource found. You must create at least\n"
-         "one before running this command.\n"));
-      return 0;
+   
+
+   i = find_arg_with_value(ua, "strip_prefix");
+   if (i >= 0) {
+      strip_prefix = ua->argv[i];
    }
 
-   /* 
-    * Request user to select JobIds by various different methods
-    *  last 20 jobs, where File saved, most recent backup, ...
-    */
-   if (!user_select_jobids(ua, &ji)) {
-      return 0;
+   i = find_arg_with_value(ua, "add_prefix");
+   if (i >= 0) {
+      add_prefix = ua->argv[i];
    }
 
-   /* 
-    * Build the directory tree containing JobIds user selected
-    */
-   tree.root = new_tree(ji.TotalFiles);
-   tree.root->fname = nofname;
-   tree.ua = ua;
-   query = get_pool_memory(PM_MESSAGE);
-   last_JobId = 0;
-   /*
-    * For display purposes, the same JobId, with different volumes may
-    * appear more than once, however, we only insert it once.
-    */
-   for (p=ji.JobIds; next_jobid_from_list(&p, &JobId) > 0; ) {
+   i = find_arg_with_value(ua, "add_suffix");
+   if (i >= 0) {
+      add_suffix = ua->argv[i];
+   }
 
-      if (JobId == last_JobId) {            
-        continue;                    /* eliminate duplicate JobIds */
-      }
-      last_JobId = JobId;
-      bsendmsg(ua, _("Building directory tree for JobId %u ...\n"), JobId);
-      /*
-       * Find files for this JobId and insert them in the tree
-       */
-      Mmsg(&query, uar_sel_files, JobId);
-      if (!db_sql_query(ua->db, query, insert_tree_handler, (void *)&tree)) {
-         bsendmsg(ua, "%s", db_strerror(ua->db));
-      }
-      /*
-       * Find the FileSets for this JobId and add to the name_list
-       */
-      Mmsg(&query, uar_mediatype, JobId);
-      if (!db_sql_query(ua->db, query, unique_name_list_handler, (void *)&name_list)) {
-         bsendmsg(ua, "%s", db_strerror(ua->db));
+   i = find_arg_with_value(ua, "regexwhere");
+   if (i >= 0) {
+      rx.RegexWhere = ua->argv[i];
+   }
+
+   if (strip_prefix || add_suffix || add_prefix) {
+      int len = bregexp_get_build_where_size(strip_prefix, add_prefix, add_suffix);
+      regexp = (char *)bmalloc(len * sizeof(char));
+
+      bregexp_build_where(regexp, len, strip_prefix, add_prefix, add_suffix);
+      rx.RegexWhere = regexp;
+   }
+
+   /* TODO: add acl for regexwhere ? */
+
+   if (rx.RegexWhere) {
+      if (!acl_access_ok(ua, Where_ACL, rx.RegexWhere)) {
+         ua->error_msg(_("\"RegexWhere\" specification not authorized.\n"));
+         goto bail_out;
       }
+   }
 
+   if (rx.where) {
+      if (!acl_access_ok(ua, Where_ACL, rx.where)) {
+         ua->error_msg(_("\"where\" specification not authorized.\n"));
+         goto bail_out;
+      }
    }
-   bsendmsg(ua, "%d items inserted into the tree and marked for extraction.\n");
-   free_pool_memory(query);
 
-   /* Check MediaType and select storage that corresponds */
-   get_storage_from_mediatype(ua, &name_list, &ji);
-   free_name_list(&name_list);
+   if (!open_client_db(ua)) {
+      goto bail_out;
+   }
 
-   /* Let the user select which files to restore */
-   user_select_files(&tree);
+   /* Ensure there is at least one Restore Job */
+   LockRes();
+   foreach_res(job, R_JOB) {
+      if (job->JobType == JT_RESTORE) {
+         if (!rx.restore_job) {
+            rx.restore_job = job;
+         }
+         rx.restore_jobs++;
+      }
+   }
+   UnlockRes();
+   if (!rx.restore_jobs) {
+      ua->error_msg(_(
+         "No Restore Job Resource found in bacula-dir.conf.\n"
+         "You must create at least one before running this command.\n"));
+      goto bail_out;
+   }
 
    /*
-    * Walk down through the tree finding all files marked to be 
-    *  extracted making a bootstrap file.
+    * Request user to select JobIds or files by various different methods
+    *  last 20 jobs, where File saved, most recent backup, ...
+    *  In the end, a list of files are pumped into
+    *  add_findex()
     */
-   bsr = new_bsr();
-   for (TREE_NODE *node=first_tree_node(tree.root); node; node=next_tree_node(node)) {
-      Dmsg2(400, "FI=%d node=0x%x\n", node->FileIndex, node);
-      if (node->extract) {
-         Dmsg2(400, "type=%d FI=%d\n", node->type, node->FileIndex);
-        add_findex(bsr, node->JobId, node->FileIndex);
-        selected_files++;
+   switch (user_select_jobids_or_files(ua, &rx)) {
+   case 0:                            /* error */
+      goto bail_out;
+   case 1:                            /* selected by jobid */
+      get_and_display_basejobs(ua, &rx);
+      if (!build_directory_tree(ua, &rx)) {
+         ua->send_msg(_("Restore not done.\n"));
+         goto bail_out;
       }
+      break;
+   case 2:                            /* selected by filename, no tree needed */
+      break;
    }
 
-   free_tree(tree.root);             /* free the directory tree */
+   if (rx.bsr->JobId) {
+      char ed1[50];
+      if (!complete_bsr(ua, rx.bsr)) {   /* find Vol, SessId, SessTime from JobIds */
+         ua->error_msg(_("Unable to construct a valid BSR. Cannot continue.\n"));
+         goto bail_out;
+      }
+      if (!(rx.selected_files = write_bsr_file(ua, rx))) {
+         ua->warning_msg(_("No files selected to be restored.\n"));
+         goto bail_out;
+      }
+      display_bsr_info(ua, rx);          /* display vols needed, etc */
 
-   if (bsr->JobId) {
-      if (!complete_bsr(ua, bsr)) {   /* find Vol, SessId, SessTime from JobIds */
-         bsendmsg(ua, _("Unable to construct a valid BSR. Cannot continue.\n"));
-        free_bsr(bsr);
-        return 0;
+      if (rx.selected_files==1) {
+         ua->info_msg(_("\n1 file selected to be restored.\n\n"));
+      } else {
+         ua->info_msg(_("\n%s files selected to be restored.\n\n"), 
+            edit_uint64_with_commas(rx.selected_files, ed1));
       }
-//    print_bsr(ua, bsr);
-      write_bsr_file(ua, bsr);
-      bsendmsg(ua, _("\n%u files selected to restore.\n\n"), selected_files);
    } else {
-      bsendmsg(ua, _("No files selected to restore.\n"));
+      ua->warning_msg(_("No files selected to be restored.\n"));
+      goto bail_out;
    }
-   free_bsr(bsr);
 
-   if (restore_jobs == 1) {
-      job = restore_job;
+   if (rx.restore_jobs == 1) {
+      job = rx.restore_job;
    } else {
-      job = select_restore_job_resource(ua);
+      job = get_restore_job(ua);
    }
    if (!job) {
-      bsendmsg(ua, _("No Restore Job resource found!\n"));
-      return 0;
+      goto bail_out;
    }
 
-   if (ji.ClientName[0]) {
-      Mmsg(&ua->cmd, 
-         "run job=\"%s\" client=\"%s\" storage=\"%s\" bootstrap=\"%s/restore.bsr\"",
-         job->hdr.name, ji.ClientName, ji.store?ji.store->hdr.name:"",
-        working_directory);
-   } else {
-      Mmsg(&ua->cmd, 
-         "run job=\"%s\" storage=\"%s\" bootstrap=\"%s/restore.bsr\"",
-         job->hdr.name, ji.store?ji.store->hdr.name:"", working_directory);
+   get_client_name(ua, &rx);
+   if (!rx.ClientName) {
+      ua->error_msg(_("No Client resource found!\n"));
+      goto bail_out;
+   }
+   get_restore_client_name(ua, rx);
+
+   escaped_bsr_name = escape_filename(jcr->RestoreBootstrap);
+
+   Mmsg(ua->cmd,
+        "run job=\"%s\" client=\"%s\" restoreclient=\"%s\" storage=\"%s\""
+        " bootstrap=\"%s\" files=%u catalog=\"%s\"",
+        job->name(), rx.ClientName, rx.RestoreClientName,
+        rx.store?rx.store->name():"",
+        escaped_bsr_name ? escaped_bsr_name : jcr->RestoreBootstrap,
+        rx.selected_files, ua->catalog->name());
+
+   /* Build run command */
+   pm_strcpy(buf, "");
+   if (rx.RegexWhere) {
+      escaped_where_name = escape_filename(rx.RegexWhere);
+      Mmsg(buf, " regexwhere=\"%s\"", 
+           escaped_where_name ? escaped_where_name : rx.RegexWhere);
+
+   } else if (rx.where) {
+      escaped_where_name = escape_filename(rx.where);
+      Mmsg(buf," where=\"%s\"", 
+           escaped_where_name ? escaped_where_name : rx.where);
+   }
+   pm_strcat(ua->cmd, buf);
+
+   if (rx.replace) {
+      Mmsg(buf, " replace=%s", rx.replace);
+      pm_strcat(ua->cmd, buf);
+   }
+
+   if (rx.comment) {
+      Mmsg(buf, " comment=\"%s\"", rx.comment);
+      pm_strcat(ua->cmd, buf);
+   }
+
+   if (escaped_bsr_name != NULL) {
+      bfree(escaped_bsr_name);
+   }
+
+   if (escaped_where_name != NULL) {
+      bfree(escaped_where_name);
    }
    
+   if (regexp) {
+      bfree(regexp);
+   }
+
+   if (find_arg(ua, NT_("yes")) > 0) {
+      pm_strcat(ua->cmd, " yes");    /* pass it on to the run command */
+   }
+   Dmsg1(200, "Submitting: %s\n", ua->cmd);
+   /* Transfer jobids to jcr to for picking up restore objects */
+   jcr->JobIds = rx.JobIds;
+   rx.JobIds = NULL;
+   parse_ua_args(ua);
+   run_cmd(ua, ua->cmd);
+   free_rx(&rx);
+   garbage_collect_memory();       /* release unused memory */
+   return 1;
+
+bail_out:
+   if (escaped_bsr_name != NULL) {
+      bfree(escaped_bsr_name);
+   }
+
+   if (escaped_where_name != NULL) {
+      bfree(escaped_where_name);
+   }
+
+   if (regexp) {
+      bfree(regexp);
+   }
+
+   free_rx(&rx);
+   garbage_collect_memory();       /* release unused memory */
+   return 0;
+
+}
+
+/* 
+ * Fill the rx->BaseJobIds and display the list
+ */
+static void get_and_display_basejobs(UAContext *ua, RESTORE_CTX *rx)
+{
+   db_list_ctx jobids;
 
-   Dmsg1(400, "Submitting: %s\n", ua->cmd);
+   if (!db_get_used_base_jobids(ua->jcr, ua->db, rx->JobIds, &jobids)) {
+      ua->warning_msg("%s", db_strerror(ua->db));
+   }
    
-   parse_command_args(ua);
-   runcmd(ua, ua->cmd);
+   if (jobids.count) {
+      POOL_MEM q;
+      Mmsg(q, uar_print_jobs, jobids.list);
+      ua->send_msg(_("The restore will use the following job(s) as Base\n"));
+      db_list_sql_query(ua->jcr, ua->db, q.c_str(), prtit, ua, 1, HORZ_LIST);
+   }
+   pm_strcpy(rx->BaseJobIds, jobids.list);
+}
+
+static void free_rx(RESTORE_CTX *rx)
+{
+   free_bsr(rx->bsr);
+   rx->bsr = NULL;
+   free_and_null_pool_memory(rx->JobIds);
+   free_and_null_pool_memory(rx->BaseJobIds);
+   free_and_null_pool_memory(rx->fname);
+   free_and_null_pool_memory(rx->path);
+   free_and_null_pool_memory(rx->query);
+   free_name_list(&rx->name_list);
+}
+
+static bool has_value(UAContext *ua, int i)
+{
+   if (!ua->argv[i]) {
+      ua->error_msg(_("Missing value for keyword: %s\n"), ua->argk[i]);
+      return false;
+   }
+   return true;
+}
+
+/*
+ * This gets the client name from which the backup was made
+ */
+static int get_client_name(UAContext *ua, RESTORE_CTX *rx)
+{
+   /* If no client name specified yet, get it now */
+   if (!rx->ClientName[0]) {
+      CLIENT_DBR cr;
+      /* try command line argument */
+      int i = find_arg_with_value(ua, NT_("client"));
+      if (i < 0) {
+         i = find_arg_with_value(ua, NT_("backupclient"));
+      }
+      if (i >= 0) {
+         if (!is_name_valid(ua->argv[i], &ua->errmsg)) {
+            ua->error_msg("%s argument: %s", ua->argk[i], ua->errmsg);
+            return 0;
+         }
+         bstrncpy(rx->ClientName, ua->argv[i], sizeof(rx->ClientName));
+         return 1;
+      }
+      memset(&cr, 0, sizeof(cr));
+      if (!get_client_dbr(ua, &cr)) {
+         return 0;
+      }
+      bstrncpy(rx->ClientName, cr.Name, sizeof(rx->ClientName));
+   }
+   return 1;
+}
 
-   bsendmsg(ua, _("Restore command done.\n"));
+/*
+ * This is where we pick up a client name to restore to.
+ */
+static int get_restore_client_name(UAContext *ua, RESTORE_CTX &rx)
+{
+   /* Start with same name as backup client */
+   bstrncpy(rx.RestoreClientName, rx.ClientName, sizeof(rx.RestoreClientName));    
+
+   /* try command line argument */
+   int i = find_arg_with_value(ua, NT_("restoreclient"));
+   if (i >= 0) {
+      if (!is_name_valid(ua->argv[i], &ua->errmsg)) {
+         ua->error_msg("%s argument: %s", ua->argk[i], ua->errmsg);
+         return 0;
+      }
+      bstrncpy(rx.RestoreClientName, ua->argv[i], sizeof(rx.RestoreClientName));
+      return 1;
+   }
    return 1;
 }
 
+
+
 /*
- * The first step in the restore process is for the user to 
+ * The first step in the restore process is for the user to
  *  select a list of JobIds from which he will subsequently
  *  select which files are to be restored.
+ *
+ *  Returns:  2  if filename list made
+ *            1  if jobid list made
+ *            0  on error
  */
-static int user_select_jobids(UAContext *ua, JobIds *ji)
+static int user_select_jobids_or_files(UAContext *ua, RESTORE_CTX *rx)
 {
-   char fileset_name[MAX_NAME_LENGTH];
-   char *p, ed1[50];
-   FILESET_DBR fsr;
-   CLIENT_DBR cr;
+   char *p;
+   char date[MAX_TIME_LENGTH];
+   bool have_date = false;
+   /* Include current second if using current time */
+   utime_t now = time(NULL) + 1;
    JobId_t JobId;
-   JOB_DBR jr;
-   POOLMEM *query;
-   int done = 0;
-   char *list[] = { 
-      "List last 20 Jobs run",
-      "List Jobs where a given File is saved",
-      "Enter list of JobIds to select",
-      "Enter SQL list command", 
-      "Select the most recent backup for a client",
-      "Cancel",
+   JOB_DBR jr = { (JobId_t)-1 };
+   bool done = false;
+   int i, j;
+   const char *list[] = {
+      _("List last 20 Jobs run"),
+      _("List Jobs where a given File is saved"),
+      _("Enter list of comma separated JobIds to select"),
+      _("Enter SQL list command"),
+      _("Select the most recent backup for a client"),
+      _("Select backup for a client before a specified time"),
+      _("Enter a list of files to restore"),
+      _("Enter a list of files to restore before a specified time"),
+      _("Find the JobIds of the most recent backup for a client"),
+      _("Find the JobIds for a backup for a client before a specified time"),
+      _("Enter a list of directories to restore for found JobIds"),
+      _("Select full restore to a specified Job date"),
+      _("Cancel"),
       NULL };
 
-   bsendmsg(ua, _("\nFirst you select one or more JobIds that contain files\n"
+   const char *kw[] = {
+       /* These keywords are handled in a for loop */
+      "jobid",       /* 0 */
+      "current",     /* 1 */
+      "before",      /* 2 */
+      "file",        /* 3 */
+      "directory",   /* 4 */
+      "select",      /* 5 */
+      "pool",        /* 6 */
+      "all",         /* 7 */
+
+      /* The keyword below are handled by individual arg lookups */
+      "client",       /* 8 */
+      "storage",      /* 9 */
+      "fileset",      /* 10 */
+      "where",        /* 11 */
+      "yes",          /* 12 */
+      "bootstrap",    /* 13 */
+      "done",         /* 14 */
+      "strip_prefix", /* 15 */
+      "add_prefix",   /* 16 */
+      "add_suffix",   /* 17 */
+      "regexwhere",   /* 18 */
+      "restoreclient", /* 19 */
+      "copies",        /* 20 */
+      "comment",       /* 21 */
+      "restorejob",    /* 22 */
+      "replace",       /* 23 */
+      NULL
+   };
+
+   rx->JobIds[0] = 0;
+
+   for (i=1; i<ua->argc; i++) {       /* loop through arguments */
+      bool found_kw = false;
+      for (j=0; kw[j]; j++) {         /* loop through keywords */
+         if (strcasecmp(kw[j], ua->argk[i]) == 0) {
+            found_kw = true;
+            break;
+         }
+      }
+      if (!found_kw) {
+         ua->error_msg(_("Unknown keyword: %s\n"), ua->argk[i]);
+         return 0;
+      }
+      /* Found keyword in kw[] list, process it */
+      switch (j) {
+      case 0:                            /* jobid */
+         if (!has_value(ua, i)) {
+            return 0;
+         }
+         if (*rx->JobIds != 0) {
+            pm_strcat(rx->JobIds, ",");
+         }
+         pm_strcat(rx->JobIds, ua->argv[i]);
+         done = true;
+         break;
+      case 1:                            /* current */
+         /*
+          * Note, we add one second here just to include any job
+          *  that may have finished within the current second,
+          *  which happens a lot in scripting small jobs.
+          */
+         bstrutime(date, sizeof(date), now);
+         have_date = true;
+         break;
+      case 2:                            /* before */
+         if (have_date || !has_value(ua, i)) {
+            return 0;
+         }
+         if (str_to_utime(ua->argv[i]) == 0) {
+            ua->error_msg(_("Improper date format: %s\n"), ua->argv[i]);
+            return 0;
+         }
+         bstrncpy(date, ua->argv[i], sizeof(date));
+         have_date = true;
+         break;
+      case 3:                            /* file */
+      case 4:                            /* dir */
+         if (!has_value(ua, i)) {
+            return 0;
+         }
+         if (!have_date) {
+            bstrutime(date, sizeof(date), now);
+         }
+         if (!get_client_name(ua, rx)) {
+            return 0;
+         }
+         pm_strcpy(ua->cmd, ua->argv[i]);
+         insert_one_file_or_dir(ua, rx, date, j==4);
+         return 2;
+      case 5:                            /* select */
+         if (!have_date) {
+            bstrutime(date, sizeof(date), now);
+         }
+         if (!select_backups_before_date(ua, rx, date)) {
+            return 0;
+         }
+         done = true;
+         break;
+      case 6:                            /* pool specified */
+         if (!has_value(ua, i)) {
+            return 0;
+         }
+         rx->pool = (POOL *)GetResWithName(R_POOL, ua->argv[i]);
+         if (!rx->pool) {
+            ua->error_msg(_("Error: Pool resource \"%s\" does not exist.\n"), ua->argv[i]);
+            return 0;
+         }
+         if (!acl_access_ok(ua, Pool_ACL, ua->argv[i])) {
+            rx->pool = NULL;
+            ua->error_msg(_("Error: Pool resource \"%s\" access not allowed.\n"), ua->argv[i]);
+            return 0;
+         }
+         break;
+      case 7:                         /* all specified */
+         rx->all = true;
+         break;
+      /*
+       * All keywords 7 or greater are ignored or handled by a select prompt
+       */
+      default:
+         break;
+      }
+   }
+
+   if (!done) {
+      ua->send_msg(_("\nFirst you select one or more JobIds that contain files\n"
                   "to be restored. You will be presented several methods\n"
                   "of specifying the JobIds. Then you will be allowed to\n"
                   "select which files from those JobIds are to be restored.\n\n"));
+   }
 
+   /* If choice not already made above, prompt */
    for ( ; !done; ) {
+      char *fname;
+      int len;
+      bool gui_save;
+      db_list_ctx jobids;
+
       start_prompt(ua, _("To select the JobIds, you have the following choices:\n"));
       for (int i=0; list[i]; i++) {
-        add_prompt(ua, list[i]);
+         add_prompt(ua, list[i]);
       }
-      done = 1;
-      switch (do_prompt(ua, "Select item: ", NULL, 0)) {
-      case -1:                       /* error */
-        return 0;
-      case 0:                        /* list last 20 Jobs run */
-        db_list_sql_query(ua->jcr, ua->db, uar_list_jobs, prtit, ua, 1);
-        done = 0;
-        break;
-      case 1:                        /* list where a file is saved */
-        char *fname;
-        int len;
-         if (!get_cmd(ua, _("Enter Filename: "))) {
-           return 0;
-        }
-        len = strlen(ua->cmd);
-        fname = (char *)malloc(len * 2 + 1);
-        db_escape_string(fname, ua->cmd, len);
-        query = get_pool_memory(PM_MESSAGE);
-        Mmsg(&query, uar_file, fname);
-        free(fname);
-        db_list_sql_query(ua->jcr, ua->db, query, prtit, ua, 1);
-        free_pool_memory(query);
-        done = 0;
-        break;
-      case 2:                        /* enter a list of JobIds */
+      done = true;
+      switch (do_prompt(ua, "", _("Select item: "), NULL, 0)) {
+      case -1:                        /* error or cancel */
+         return 0;
+      case 0:                         /* list last 20 Jobs run */
+         if (!acl_access_ok(ua, Command_ACL, NT_("sqlquery"), 8)) {
+            ua->error_msg(_("SQL query not authorized.\n"));
+            return 0;
+         }
+         gui_save = ua->jcr->gui;
+         ua->jcr->gui = true;
+         db_list_sql_query(ua->jcr, ua->db, uar_list_jobs, prtit, ua, 1, HORZ_LIST);
+         ua->jcr->gui = gui_save;
+         done = false;
+         break;
+      case 1:                         /* list where a file is saved */
+         if (!get_client_name(ua, rx)) {
+            return 0;
+         }
+         if (!get_cmd(ua, _("Enter Filename (no path):"))) {
+            return 0;
+         }
+         len = strlen(ua->cmd);
+         fname = (char *)malloc(len * 2 + 1);
+         db_escape_string(ua->jcr, ua->db, fname, ua->cmd, len);
+         Mmsg(rx->query, uar_file[db_get_type_index(ua->db)], rx->ClientName, fname);
+         free(fname);
+         gui_save = ua->jcr->gui;
+         ua->jcr->gui = true;
+         db_list_sql_query(ua->jcr, ua->db, rx->query, prtit, ua, 1, HORZ_LIST);
+         ua->jcr->gui = gui_save;
+         done = false;
+         break;
+      case 2:                         /* enter a list of JobIds */
          if (!get_cmd(ua, _("Enter JobId(s), comma separated, to restore: "))) {
-           return 0;
-        }
-        bstrncpy(ji->JobIds, ua->cmd, sizeof(ji->JobIds));
-        break;
-      case 3:                        /* Enter an SQL list command */
+            return 0;
+         }
+         pm_strcpy(rx->JobIds, ua->cmd);
+         break;
+      case 3:                         /* Enter an SQL list command */
+         if (!acl_access_ok(ua, Command_ACL, NT_("sqlquery"), 8)) {
+            ua->error_msg(_("SQL query not authorized.\n"));
+            return 0;
+         }
          if (!get_cmd(ua, _("Enter SQL list command: "))) {
-           return 0;
-        }
-        db_list_sql_query(ua->jcr, ua->db, ua->cmd, prtit, ua, 1);
-        done = 0;
-        break;
-      case 4:                        /* Select the most recent backups */
-        query = get_pool_memory(PM_MESSAGE);
-        db_sql_query(ua->db, uar_del_temp, NULL, NULL);
-        db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
-        if (!db_sql_query(ua->db, uar_create_temp, NULL, NULL)) {
-            bsendmsg(ua, "%s\n", db_strerror(ua->db));
-        }
-        if (!db_sql_query(ua->db, uar_create_temp1, NULL, NULL)) {
-            bsendmsg(ua, "%s\n", db_strerror(ua->db));
-        }
-        /*
-         * Select Client from the Catalog
-         */
-        memset(&cr, 0, sizeof(cr));
-        if (!get_client_dbr(ua, &cr)) {
-           free_pool_memory(query);
-           db_sql_query(ua->db, uar_del_temp, NULL, NULL);
-           db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
-           return 0;
-        }
-        bstrncpy(ji->ClientName, cr.Name, sizeof(ji->ClientName));
-
-        /*
-         * Select FileSet 
-         */
-        Mmsg(&query, uar_sel_fileset, cr.ClientId, cr.ClientId);
-         start_prompt(ua, _("The defined FileSet resources are:\n"));
-        if (!db_sql_query(ua->db, query, fileset_handler, (void *)ua)) {
-            bsendmsg(ua, "%s\n", db_strerror(ua->db));
-        }
-         if (do_prompt(ua, _("Select FileSet resource"), 
-                      fileset_name, sizeof(fileset_name)) < 0) {
-           free_pool_memory(query);
-           db_sql_query(ua->db, uar_del_temp, NULL, NULL);
-           db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
-           return 0;
-        }
-        fsr.FileSetId = atoi(fileset_name);  /* Id is first part of name */
-        if (!db_get_fileset_record(ua->jcr, ua->db, &fsr)) {
-            bsendmsg(ua, _("Error getting FileSet record: %s\n"), db_strerror(ua->db));
-            bsendmsg(ua, _("This probably means you modified the FileSet.\n"
-                           "Continuing anyway.\n"));
-        }
-
-        /* Find JobId of last Full backup for this client, fileset */
-        Mmsg(&query, uar_last_full, cr.ClientId, cr.ClientId, fsr.FileSetId);
-        if (!db_sql_query(ua->db, query, NULL, NULL)) {
-            bsendmsg(ua, "%s\n", db_strerror(ua->db));
-        }
-        /* Find all Volumes used by that JobId */
-        if (!db_sql_query(ua->db, uar_full, NULL,NULL)) {
-            bsendmsg(ua, "%s\n", db_strerror(ua->db));
-        }
-         /* Note, this is needed as I don't seem to get the callback
-         * from the call just above.
-         */
-        if (!db_sql_query(ua->db, uar_sel_all_temp1, last_full_handler, (void *)ji)) {
-            bsendmsg(ua, "%s\n", db_strerror(ua->db));
-        }
-        /* Now find all Incremental Jobs */
-        Mmsg(&query, uar_inc, edit_uint64(ji->JobTDate, ed1), cr.ClientId, fsr.FileSetId);
-        if (!db_sql_query(ua->db, query, NULL, NULL)) {
-            bsendmsg(ua, "%s\n", db_strerror(ua->db));
-        }
-        free_pool_memory(query);
-        db_list_sql_query(ua->jcr, ua->db, uar_list_temp, prtit, ua, 1);
-
-        if (!db_sql_query(ua->db, uar_sel_jobid_temp, jobid_handler, (void *)ji)) {
-            bsendmsg(ua, "%s\n", db_strerror(ua->db));
-        }
-        db_sql_query(ua->db, uar_del_temp, NULL, NULL);
-        db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
-        break;
-      case 5:
-        return 0;
+            return 0;
+         }
+         gui_save = ua->jcr->gui;
+         ua->jcr->gui = true;
+         db_list_sql_query(ua->jcr, ua->db, ua->cmd, prtit, ua, 1, HORZ_LIST);
+         ua->jcr->gui = gui_save;
+         done = false;
+         break;
+      case 4:                         /* Select the most recent backups */
+         if (!have_date) {
+            bstrutime(date, sizeof(date), now);
+         }
+         if (!select_backups_before_date(ua, rx, date)) {
+            return 0;
+         }
+         break;
+      case 5:                         /* select backup at specified time */
+         if (!have_date) {
+            if (!get_date(ua, date, sizeof(date))) {
+               return 0;
+            }
+         }
+         if (!select_backups_before_date(ua, rx, date)) {
+            return 0;
+         }
+         break;
+      case 6:                         /* Enter files */
+         if (!have_date) {
+            bstrutime(date, sizeof(date), now);
+         }
+         if (!get_client_name(ua, rx)) {
+            return 0;
+         }
+         ua->send_msg(_("Enter file names with paths, or < to enter a filename\n"
+                        "containing a list of file names with paths, and terminate\n"
+                        "them with a blank line.\n"));
+         for ( ;; ) {
+            if (!get_cmd(ua, _("Enter full filename: "))) {
+               return 0;
+            }
+            len = strlen(ua->cmd);
+            if (len == 0) {
+               break;
+            }
+            insert_one_file_or_dir(ua, rx, date, false);
+         }
+         return 2;
+       case 7:                        /* enter files backed up before specified time */
+         if (!have_date) {
+            if (!get_date(ua, date, sizeof(date))) {
+               return 0;
+            }
+         }
+         if (!get_client_name(ua, rx)) {
+            return 0;
+         }
+         ua->send_msg(_("Enter file names with paths, or < to enter a filename\n"
+                        "containing a list of file names with paths, and terminate\n"
+                        "them with a blank line.\n"));
+         for ( ;; ) {
+            if (!get_cmd(ua, _("Enter full filename: "))) {
+               return 0;
+            }
+            len = strlen(ua->cmd);
+            if (len == 0) {
+               break;
+            }
+            insert_one_file_or_dir(ua, rx, date, false);
+         }
+         return 2;
+
+      case 8:                         /* Find JobIds for current backup */
+         if (!have_date) {
+            bstrutime(date, sizeof(date), now);
+         }
+         if (!select_backups_before_date(ua, rx, date)) {
+            return 0;
+         }
+         done = false;
+         break;
+
+      case 9:                         /* Find JobIds for give date */
+         if (!have_date) {
+            if (!get_date(ua, date, sizeof(date))) {
+               return 0;
+            }
+         }
+         if (!select_backups_before_date(ua, rx, date)) {
+            return 0;
+         }
+         done = false;
+         break;
+
+      case 10:                        /* Enter directories */
+         if (*rx->JobIds != 0) {
+            ua->send_msg(_("You have already selected the following JobIds: %s\n"),
+               rx->JobIds);
+         } else if (get_cmd(ua, _("Enter JobId(s), comma separated, to restore: "))) {
+            if (*rx->JobIds != 0 && *ua->cmd) {
+               pm_strcat(rx->JobIds, ",");
+            }
+            pm_strcat(rx->JobIds, ua->cmd);
+         }
+         if (*rx->JobIds == 0 || *rx->JobIds == '.') {
+            *rx->JobIds = 0;
+            return 0;                 /* nothing entered, return */
+         }
+         if (!have_date) {
+            bstrutime(date, sizeof(date), now);
+         }
+         if (!get_client_name(ua, rx)) {
+            return 0;
+         }
+         ua->send_msg(_("Enter full directory names or start the name\n"
+                        "with a < to indicate it is a filename containing a list\n"
+                        "of directories and terminate them with a blank line.\n"));
+         for ( ;; ) {
+            if (!get_cmd(ua, _("Enter directory name: "))) {
+               return 0;
+            }
+            len = strlen(ua->cmd);
+            if (len == 0) {
+               break;
+            }
+            /* Add trailing slash to end of directory names */
+            if (ua->cmd[0] != '<' && !IsPathSeparator(ua->cmd[len-1])) {
+               strcat(ua->cmd, "/");
+            }
+            insert_one_file_or_dir(ua, rx, date, true);
+         }
+         return 2;
+
+      case 11:                        /* Choose a jobid and select jobs */
+         if (!get_cmd(ua, _("Enter JobId to get the state to restore: ")) ||
+             !is_an_integer(ua->cmd)) 
+         {
+            return 0;
+         }
+
+         memset(&jr, 0, sizeof(JOB_DBR));
+         jr.JobId = str_to_int64(ua->cmd);
+         if (!db_get_job_record(ua->jcr, ua->db, &jr)) {
+            ua->error_msg(_("Unable to get Job record for JobId=%s: ERR=%s\n"),
+                          ua->cmd, db_strerror(ua->db));
+            return 0;
+         }
+         ua->send_msg(_("Selecting jobs to build the Full state at %s\n"),
+                      jr.cStartTime);
+         jr.JobLevel = L_INCREMENTAL; /* Take Full+Diff+Incr */
+         if (!db_accurate_get_jobids(ua->jcr, ua->db, &jr, &jobids)) {
+            return 0;
+         }
+         pm_strcpy(rx->JobIds, jobids.list);
+         Dmsg1(30, "Item 12: jobids = %s\n", rx->JobIds);
+         break;
+      case 12:                        /* Cancel or quit */
+         return 0;
       }
    }
 
-   if (*ji->JobIds == 0) {
-      bsendmsg(ua, _("No Jobs selected.\n"));
-      return 0;
-   }
-   bsendmsg(ua, _("You have selected the following JobId: %s\n"), ji->JobIds);
-
    memset(&jr, 0, sizeof(JOB_DBR));
-
-   ji->TotalFiles = 0;
-   for (p=ji->JobIds; ; ) {
-      int stat = next_jobid_from_list(&p, &JobId);
+   POOLMEM *JobIds = get_pool_memory(PM_FNAME);
+   *JobIds = 0;
+   rx->TotalFiles = 0;
+   /*        
+    * Find total number of files to be restored, and filter the JobId
+    *  list to contain only ones permitted by the ACL conditions.
+    */
+   for (p=rx->JobIds; ; ) {
+      char ed1[50];
+      int stat = get_next_jobid_from_list(&p, &JobId);
       if (stat < 0) {
-         bsendmsg(ua, _("Invalid JobId in list.\n"));
-        return 0;
+         ua->error_msg(_("Invalid JobId in list.\n"));
+         free_pool_memory(JobIds);
+         return 0;
       }
       if (stat == 0) {
-        break;
+         break;
+      }
+      if (jr.JobId == JobId) {
+         continue;                    /* duplicate of last JobId */
       }
+      memset(&jr, 0, sizeof(JOB_DBR));
       jr.JobId = JobId;
       if (!db_get_job_record(ua->jcr, ua->db, &jr)) {
-         bsendmsg(ua, _("Unable to get Job record. ERR=%s\n"), db_strerror(ua->db));
-        return 0;
+         ua->error_msg(_("Unable to get Job record for JobId=%s: ERR=%s\n"),
+            edit_int64(JobId, ed1), db_strerror(ua->db));
+         free_pool_memory(JobIds);
+         return 0;
       }
-      ji->TotalFiles += jr.JobFiles;
-   }
-   return 1;
-}
-
-static int next_jobid_from_list(char **p, uint32_t *JobId)
-{
-   char jobid[30];
-   int i;
-   char *q = *p;
-
-   jobid[0] = 0;
-   for (i=0; i<(int)sizeof(jobid); i++) {
-      if (*q == ',' || *q == 0) {
-        q++;
-        break;
+      if (!acl_access_ok(ua, Job_ACL, jr.Name)) {
+         ua->error_msg(_("Access to JobId=%s (Job \"%s\") not authorized. Not selected.\n"),
+            edit_int64(JobId, ed1), jr.Name);
+         continue;
+      }
+      if (*JobIds != 0) {
+         pm_strcat(JobIds, ",");
       }
-      jobid[i] = *q++;
-      jobid[i+1] = 0;
+      pm_strcat(JobIds, edit_int64(JobId, ed1));
+      rx->TotalFiles += jr.JobFiles;
    }
-   if (jobid[0] == 0 || !is_a_number(jobid)) {
+   free_pool_memory(rx->JobIds);
+   rx->JobIds = JobIds;               /* Set ACL filtered list */
+   if (*rx->JobIds == 0) {
+      ua->warning_msg(_("No Jobs selected.\n"));
       return 0;
    }
-   *p = q;
-   *JobId = strtoul(jobid, NULL, 10);
+
+   if (strchr(rx->JobIds,',')) {
+      ua->info_msg(_("You have selected the following JobIds: %s\n"), rx->JobIds);
+   } else {
+      ua->info_msg(_("You have selected the following JobId: %s\n"), rx->JobIds);
+   }
    return 1;
 }
 
 /*
- * Callback handler make list of JobIds
+ * Get date from user
  */
-static int jobid_handler(void *ctx, int num_fields, char **row)
+static bool get_date(UAContext *ua, char *date, int date_len)
 {
-   JobIds *ji = (JobIds *)ctx;
-
-   if (strlen(ji->JobIds)+strlen(row[0])+2 < sizeof(ji->JobIds)) {
-      if (ji->JobIds[0] != 0) {
-         strcat(ji->JobIds, ",");
+   ua->send_msg(_("The restored files will the most current backup\n"
+                  "BEFORE the date you specify below.\n\n"));
+   for ( ;; ) {
+      if (!get_cmd(ua, _("Enter date as YYYY-MM-DD HH:MM:SS :"))) {
+         return false;
       }
-      strcat(ji->JobIds, row[0]);
+      if (str_to_utime(ua->cmd) != 0) {
+         break;
+      }
+      ua->error_msg(_("Improper date format.\n"));
    }
-
-   return 0;
+   bstrncpy(date, ua->cmd, date_len);
+   return true;
 }
 
-
 /*
- * Callback handler to pickup last Full backup JobId and ClientId
+ * Insert a single file, or read a list of files from a file
  */
-static int last_full_handler(void *ctx, int num_fields, char **row)
+static void insert_one_file_or_dir(UAContext *ua, RESTORE_CTX *rx, char *date, bool dir)
 {
-   JobIds *ji = (JobIds *)ctx;
-
-   ji->JobTDate = strtoll(row[1], NULL, 10);
-
-   return 0;
+   FILE *ffd;
+   char file[5000];
+   char *p = ua->cmd;
+   int line = 0;
+
+   switch (*p) {
+   case '<':
+      p++;
+      if ((ffd = fopen(p, "rb")) == NULL) {
+         berrno be;
+         ua->error_msg(_("Cannot open file %s: ERR=%s\n"),
+            p, be.bstrerror());
+         break;
+      }
+      while (fgets(file, sizeof(file), ffd)) {
+         line++;
+         if (dir) {
+            if (!insert_dir_into_findex_list(ua, rx, file, date)) {
+               ua->error_msg(_("Error occurred on line %d of file \"%s\"\n"), line, p);
+            }
+         } else {
+            if (!insert_file_into_findex_list(ua, rx, file, date)) {
+               ua->error_msg(_("Error occurred on line %d of file \"%s\"\n"), line, p);
+            }
+         }
+      }
+      fclose(ffd);
+      break;
+   case '?':
+      p++;
+      insert_table_into_findex_list(ua, rx, p);
+      break;
+   default:
+      if (dir) {
+         insert_dir_into_findex_list(ua, rx, ua->cmd, date);
+      } else {
+         insert_file_into_findex_list(ua, rx, ua->cmd, date);
+      }
+      break;
+   }
 }
 
 /*
- * Callback handler build fileset prompt list
+ * For a given file (path+filename), split into path and file, then
+ *   lookup the most recent backup in the catalog to get the JobId
+ *   and FileIndex, then insert them into the findex list.
  */
-static int fileset_handler(void *ctx, int num_fields, char **row)
+static bool insert_file_into_findex_list(UAContext *ua, RESTORE_CTX *rx, char *file,
+                                        char *date)
 {
-   char prompt[MAX_NAME_LENGTH+200];
-
-   snprintf(prompt, sizeof(prompt), "%s  %s  %s", row[0], row[1], row[2]);
-   add_prompt((UAContext *)ctx, prompt);
-   return 0;
+   strip_trailing_newline(file);
+   split_path_and_filename(ua, rx, file);
+   if (*rx->JobIds == 0) {
+      Mmsg(rx->query, uar_jobid_fileindex, date, rx->path, rx->fname, 
+           rx->ClientName);
+   } else {
+      Mmsg(rx->query, uar_jobids_fileindex, rx->JobIds, date,
+           rx->path, rx->fname, rx->ClientName);
+   }
+   rx->found = false;
+   /* Find and insert jobid and File Index */
+   if (!db_sql_query(ua->db, rx->query, jobid_fileindex_handler, (void *)rx)) {
+      ua->error_msg(_("Query failed: %s. ERR=%s\n"),
+         rx->query, db_strerror(ua->db));
+   }
+   if (!rx->found) {
+      ua->error_msg(_("No database record found for: %s\n"), file);
+//    ua->error_msg("Query=%s\n", rx->query);
+      return true;
+   }
+   return true;
 }
 
-/* Forward referenced commands */
-
-static int markcmd(UAContext *ua, TREE_CTX *tree);
-static int countcmd(UAContext *ua, TREE_CTX *tree);
-static int findcmd(UAContext *ua, TREE_CTX *tree);
-static int lscmd(UAContext *ua, TREE_CTX *tree);
-static int dircmd(UAContext *ua, TREE_CTX *tree);
-static int helpcmd(UAContext *ua, TREE_CTX *tree);
-static int cdcmd(UAContext *ua, TREE_CTX *tree);
-static int pwdcmd(UAContext *ua, TREE_CTX *tree);
-static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
-static int quitcmd(UAContext *ua, TREE_CTX *tree);
-
-
-struct cmdstruct { char *key; int (*func)(UAContext *ua, TREE_CTX *tree); char *help; }; 
-static struct cmdstruct commands[] = {
- { N_("mark"),       markcmd,      _("mark file for restoration")},
- { N_("unmark"),     unmarkcmd,    _("unmark file for restoration")},
- { N_("cd"),         cdcmd,        _("change current directory")},
- { N_("pwd"),        pwdcmd,       _("print current working directory")},
- { N_("ls"),         lscmd,        _("list current directory")},    
- { N_("dir"),        dircmd,       _("list current directory")},    
- { N_("count"),      countcmd,     _("count marked files")},
- { N_("find"),       findcmd,      _("find files")},
- { N_("done"),       quitcmd,      _("leave file selection mode")},
- { N_("exit"),       quitcmd,      _("exit = done")},
- { N_("help"),       helpcmd,      _("print help")},
- { N_("?"),          helpcmd,      _("print help")},    
-            };
-#define comsize (sizeof(commands)/sizeof(struct cmdstruct))
-
-
 /*
- * Enter a prompt mode where the user can select/deselect
- *  files to be restored. This is sort of like a mini-shell
- *  that allows "cd", "pwd", "add", "rm", ...
+ * For a given path lookup the most recent backup in the catalog
+ * to get the JobId and FileIndexes of all files in that directory.
  */
-static void user_select_files(TREE_CTX *tree)
-{
-   char cwd[2000];
-
-   bsendmsg(tree->ua, _( 
-      "\nYou are now entering file selection mode where you add and\n"
-      "remove files to be restored. All files are initially added.\n"
-      "Enter \"done\" to leave this mode.\n\n"));
-   /*
-    * Enter interactive command handler allowing selection
-    *  of individual files.
-    */
-   tree->node = (TREE_NODE *)tree->root;
-   tree_getpath(tree->node, cwd, sizeof(cwd));
-   bsendmsg(tree->ua, _("cwd is: %s\n"), cwd);
-   for ( ;; ) {       
-      int found, len, stat, i;
-      if (!get_cmd(tree->ua, "$ ")) {
-        break;
-      }
-      parse_command_args(tree->ua);
-      if (tree->ua->argc == 0) {
-        return;
-      }
-
-      len = strlen(tree->ua->argk[0]);
-      found = 0;
-      stat = 0;
-      for (i=0; i<(int)comsize; i++)      /* search for command */
-        if (strncasecmp(tree->ua->argk[0],  _(commands[i].key), len) == 0) {
-           stat = (*commands[i].func)(tree->ua, tree);   /* go execute command */
-           found = 1;
-           break;
-        }
-      if (!found) {
-         bsendmsg(tree->ua, _("Illegal command. Enter \"done\" to exit.\n"));
-        continue;
-      }
-      if (!stat) {
-        break;
-      }
+static bool insert_dir_into_findex_list(UAContext *ua, RESTORE_CTX *rx, char *dir,
+                                        char *date)
+{  
+   strip_trailing_junk(dir);
+   if (*rx->JobIds == 0) {
+      ua->error_msg(_("No JobId specified cannot continue.\n"));
+      return false;
+   } else {
+      Mmsg(rx->query, uar_jobid_fileindex_from_dir[db_get_type_index(ua->db)], rx->JobIds, dir, rx->ClientName);
    }
+   rx->found = false;
+   /* Find and insert jobid and File Index */
+   if (!db_sql_query(ua->db, rx->query, jobid_fileindex_handler, (void *)rx)) {
+      ua->error_msg(_("Query failed: %s. ERR=%s\n"),
+         rx->query, db_strerror(ua->db));
+   }
+   if (!rx->found) {
+      ua->error_msg(_("No database record found for: %s\n"), dir);
+      return true;
+   }
+   return true;
 }
 
 /*
- * Create new FileIndex entry for BSR 
+ * Get the JobId and FileIndexes of all files in the specified table
  */
-static RBSR_FINDEX *new_findex() 
-{
-   RBSR_FINDEX *fi = (RBSR_FINDEX *)bmalloc(sizeof(RBSR_FINDEX));
-   memset(fi, 0, sizeof(RBSR_FINDEX));
-   return fi;
-}
-
-/* Free all BSR FileIndex entries */
-static void free_findex(RBSR_FINDEX *fi)
+static bool insert_table_into_findex_list(UAContext *ua, RESTORE_CTX *rx, char *table)
 {
-   if (fi) {
-      free_findex(fi->next);
-      free(fi);
+   strip_trailing_junk(table);
+   Mmsg(rx->query, uar_jobid_fileindex_from_table, table);
+
+   rx->found = false;
+   /* Find and insert jobid and File Index */
+   if (!db_sql_query(ua->db, rx->query, jobid_fileindex_handler, (void *)rx)) {
+      ua->error_msg(_("Query failed: %s. ERR=%s\n"),
+         rx->query, db_strerror(ua->db));
+   }
+   if (!rx->found) {
+      ua->error_msg(_("No table found: %s\n"), table);
+      return true;
    }
+   return true;
 }
 
-static void write_findex(UAContext *ua, RBSR_FINDEX *fi, FILE *fd) 
+static void split_path_and_filename(UAContext *ua, RESTORE_CTX *rx, char *name)
 {
-   if (fi) {
-      if (fi->findex == fi->findex2) {
-         fprintf(fd, "FileIndex=%d\n", fi->findex);
-      } else {
-         fprintf(fd, "FileIndex=%d-%d\n", fi->findex, fi->findex2);
+   char *p, *f;
+
+   /* Find path without the filename.
+    * I.e. everything after the last / is a "filename".
+    * OK, maybe it is a directory name, but we treat it like
+    * a filename. If we don't find a / then the whole name
+    * must be a path name (e.g. c:).
+    */
+   for (p=f=name; *p; p++) {
+      if (IsPathSeparator(*p)) {
+         f = p;                       /* set pos of last slash */
       }
-      write_findex(ua, fi->next, fd);
    }
-}
+   if (IsPathSeparator(*f)) {         /* did we find a slash? */
+      f++;                            /* yes, point to filename */
+   } else {                           /* no, whole thing must be path name */
+      f = p;
+   }
 
+   /* If filename doesn't exist (i.e. root directory), we
+    * simply create a blank name consisting of a single
+    * space. This makes handling zero length filenames
+    * easier.
+    */
+   rx->fnl = p - f;
+   if (rx->fnl > 0) {
+      rx->fname = check_pool_memory_size(rx->fname, 2*(rx->fnl)+1);
+      db_escape_string(ua->jcr, ua->db, rx->fname, f, rx->fnl);
+   } else {
+      rx->fname[0] = 0;
+      rx->fnl = 0;
+   }
 
-static void print_findex(UAContext *ua, RBSR_FINDEX *fi)
-{
-   if (fi) {
-      if (fi->findex == fi->findex2) {
-         bsendmsg(ua, "FileIndex=%d\n", fi->findex);
-      } else {
-         bsendmsg(ua, "FileIndex=%d-%d\n", fi->findex, fi->findex2);
-      }
-      print_findex(ua, fi->next);
+   rx->pnl = f - name;
+   if (rx->pnl > 0) {
+      rx->path = check_pool_memory_size(rx->path, 2*(rx->pnl)+1);
+      db_escape_string(ua->jcr, ua->db, rx->path, name, rx->pnl);
+   } else {
+      rx->path[0] = 0;
+      rx->pnl = 0;
    }
-}
 
-/* Create a new bootstrap record */
-static RBSR *new_bsr()
-{
-   RBSR *bsr = (RBSR *)bmalloc(sizeof(RBSR));
-   memset(bsr, 0, sizeof(RBSR));
-   return bsr;
+   Dmsg2(100, "split path=%s file=%s\n", rx->path, rx->fname);
 }
 
-/* Free the entire BSR */
-static void free_bsr(RBSR *bsr)
+static bool ask_for_fileregex(UAContext *ua, RESTORE_CTX *rx)
 {
-   if (bsr) {
-      free_findex(bsr->fi);
-      free_bsr(bsr->next);
-      if (bsr->VolParams) {
-        free(bsr->VolParams);
-      }
-      free(bsr);
+   if (find_arg(ua, NT_("all")) >= 0) {  /* if user enters all on command line */
+      return true;                       /* select everything */
    }
-}
-
-/*
- * Complete the BSR by filling in the VolumeName and
- *  VolSessionId and VolSessionTime using the JobId
- */
-static int complete_bsr(UAContext *ua, RBSR *bsr)
-{
-   JOB_DBR jr;
-
-   if (bsr) {
-      memset(&jr, 0, sizeof(jr));
-      jr.JobId = bsr->JobId;
-      if (!db_get_job_record(ua->jcr, ua->db, &jr)) {
-         bsendmsg(ua, _("Unable to get Job record. ERR=%s\n"), db_strerror(ua->db));
-        return 0;
+   ua->send_msg(_("\n\nFor one or more of the JobIds selected, no files were found,\n"
+                 "so file selection is not possible.\n"
+                 "Most likely your retention policy pruned the files.\n"));
+   if (get_yesno(ua, _("\nDo you want to restore all the files? (yes|no): "))) {
+      if (ua->pint32_val == 1)
+         return true;
+      while (get_cmd(ua, _("\nRegexp matching files to restore? (empty to abort): "))) {
+         if (ua->cmd[0] == '\0') {
+            break;
+         } else {
+            regex_t *fileregex_re = NULL;
+            int rc;
+            char errmsg[500] = "";
+
+            fileregex_re = (regex_t *)bmalloc(sizeof(regex_t));
+            rc = regcomp(fileregex_re, ua->cmd, REG_EXTENDED|REG_NOSUB);
+            if (rc != 0) {
+               regerror(rc, fileregex_re, errmsg, sizeof(errmsg));
+            }
+            regfree(fileregex_re);
+            free(fileregex_re);
+            if (*errmsg) {
+               ua->send_msg(_("Regex compile error: %s\n"), errmsg);
+            } else {
+               rx->bsr->fileregex = bstrdup(ua->cmd);
+               return true;
+            }
+         }
       }
-      bsr->VolSessionId = jr.VolSessionId;
-      bsr->VolSessionTime = jr.VolSessionTime;
-      if ((bsr->VolCount=db_get_job_volume_parameters(ua->jcr, ua->db, bsr->JobId, 
-          &(bsr->VolParams))) == 0) {
-         bsendmsg(ua, _("Unable to get Job Volume Parameters. ERR=%s\n"), db_strerror(ua->db));
-        if (bsr->VolParams) {
-           free(bsr->VolParams);
-           bsr->VolParams = NULL;
-        }
-        return 0;
-      }
-      return complete_bsr(ua, bsr->next);
    }
-   return 1;
+   return false;
 }
 
-/*
- * Write the bootstrap record to file
+/* Walk on the delta_list of a TREE_NODE item and insert all parts
+ * TODO: Optimize for bootstrap creation, remove recursion
+ * 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> 0
+ * should insert as
+ * 0, 1, 2, 3, 4, 5, 6
  */
-static int write_bsr_file(UAContext *ua, RBSR *bsr)
+static void add_delta_list_findex(RESTORE_CTX *rx, struct delta_list *lst)
 {
-   FILE *fd;
-   POOLMEM *fname = get_pool_memory(PM_MESSAGE);
-   int stat;
-   RBSR *nbsr;
-
-   Mmsg(&fname, "%s/restore.bsr", working_directory);
-   fd = fopen(fname, "w+");
-   if (!fd) {
-      bsendmsg(ua, _("Unable to create bootstrap file %s. ERR=%s\n"), 
-        fname, strerror(errno));
-      free_pool_memory(fname);
-      return 0;
-   }
-   write_bsr(ua, bsr, fd);
-   stat = !ferror(fd);
-   fclose(fd);
-   bsendmsg(ua, _("Bootstrap records written to %s\n"), fname);
-   bsendmsg(ua, _("\nThe restore job will require the following Volumes:\n"));
-   /* Create Unique list of Volumes using prompt list */
-   start_prompt(ua, "");
-   for (nbsr=bsr; nbsr; nbsr=nbsr->next) {
-      for (int i=0; i < nbsr->VolCount; i++) {
-        add_prompt(ua, nbsr->VolParams[i].VolumeName);
-      }
+   if (lst == NULL) {
+      return;
    }
-   for (int i=0; i < ua->num_prompts; i++) {
-      bsendmsg(ua, "   %s\n", ua->prompt[i]);
-      free(ua->prompt[i]);
+   if (lst->next) {
+      add_delta_list_findex(rx, lst->next);
    }
-   ua->num_prompts = 0;
-   bsendmsg(ua, "\n");
-   free_pool_memory(fname);
-   return stat;
+   add_findex(rx->bsr, lst->JobId, lst->FileIndex);
 }
 
-static void write_bsr(UAContext *ua, RBSR *bsr, FILE *fd)
+static bool build_directory_tree(UAContext *ua, RESTORE_CTX *rx)
 {
-   if (bsr) {
-      for (int i=0; i < bsr->VolCount; i++) {
-         fprintf(fd, "Volume=\"%s\"\n", bsr->VolParams[i].VolumeName);
-         fprintf(fd, "VolSessionId=%u\n", bsr->VolSessionId);
-         fprintf(fd, "VolSessionTime=%u\n", bsr->VolSessionTime);
-         fprintf(fd, "VolFile=%u-%u\n", bsr->VolParams[i].StartFile, 
-                bsr->VolParams[i].EndFile);
-         fprintf(fd, "VolBlock=%u-%u\n", bsr->VolParams[i].StartBlock,
-                bsr->VolParams[i].EndBlock);
-        write_findex(ua, bsr->fi, fd);
-      }
-      write_bsr(ua, bsr->next, fd);
-   }
-}
+   TREE_CTX tree;
+   JobId_t JobId, last_JobId;
+   char *p;
+   bool OK = true;
+   char ed1[50];
 
-static void print_bsr(UAContext *ua, RBSR *bsr)
-{
-   if (bsr) {
-      for (int i=0; i < bsr->VolCount; i++) {
-         bsendmsg(ua, "Volume=\"%s\"\n", bsr->VolParams[i].VolumeName);
-         bsendmsg(ua, "VolSessionId=%u\n", bsr->VolSessionId);
-         bsendmsg(ua, "VolSessionTime=%u\n", bsr->VolSessionTime);
-         bsendmsg(ua, "VolFile=%u-%u\n", bsr->VolParams[i].StartFile, 
-                 bsr->VolParams[i].EndFile);
-         bsendmsg(ua, "VolBlock=%u-%u\n", bsr->VolParams[i].StartBlock,
-                 bsr->VolParams[i].EndBlock);
-        print_findex(ua, bsr->fi);
+   memset(&tree, 0, sizeof(TREE_CTX));
+   /*
+    * Build the directory tree containing JobIds user selected
+    */
+   tree.root = new_tree(rx->TotalFiles);
+   tree.ua = ua;
+   tree.all = rx->all;
+   last_JobId = 0;
+   /*
+    * For display purposes, the same JobId, with different volumes may
+    * appear more than once, however, we only insert it once.
+    */
+   p = rx->JobIds;
+   tree.FileEstimate = 0;
+   if (get_next_jobid_from_list(&p, &JobId) > 0) {
+      /* Use first JobId as estimate of the number of files to restore */
+      Mmsg(rx->query, uar_count_files, edit_int64(JobId, ed1));
+      if (!db_sql_query(ua->db, rx->query, restore_count_handler, (void *)rx)) {
+         ua->error_msg("%s\n", db_strerror(ua->db));
+      }
+      if (rx->found) {
+         /* Add about 25% more than this job for over estimate */
+         tree.FileEstimate = rx->JobId + (rx->JobId >> 2);
+         tree.DeltaCount = rx->JobId/50; /* print 50 ticks */
       }
-      print_bsr(ua, bsr->next);
    }
-}
 
-
-/*
- * Add a FileIndex to the list of BootStrap records.
- *  Here we are only dealing with JobId's and the FileIndexes
- *  associated with those JobIds.
- */
-static void add_findex(RBSR *bsr, uint32_t JobId, int32_t findex)
-{
-   RBSR *nbsr;
-   RBSR_FINDEX *fi, *lfi;
-
-   if (findex == 0) {
-      return;                        /* probably a dummy directory */
+   ua->info_msg(_("\nBuilding directory tree for JobId(s) %s ...  "),
+                rx->JobIds);
+
+#define new_get_file_list
+#ifdef new_get_file_list
+   if (!db_get_file_list(ua->jcr, ua->db, 
+                         rx->JobIds, false /* do not use md5 */, 
+                         true /* get delta */,
+                         insert_tree_handler, (void *)&tree))
+   {
+      ua->error_msg("%s", db_strerror(ua->db));
    }
-   
-   if (!bsr->fi) {                   /* if no FI add one */
-      /* This is the first FileIndex item in the chain */
-      bsr->fi = new_findex();
-      bsr->JobId = JobId;
-      bsr->fi->findex = findex;
-      bsr->fi->findex2 = findex;
-      return;
+   if (*rx->BaseJobIds) {
+      pm_strcat(rx->JobIds, ",");
+      pm_strcat(rx->JobIds, rx->BaseJobIds);
    }
-   /* Walk down list of bsrs until we find the JobId */
-   if (bsr->JobId != JobId) {
-      for (nbsr=bsr->next; nbsr; nbsr=nbsr->next) {
-        if (nbsr->JobId == JobId) {
-           bsr = nbsr;
-           break;
-        }
-      }
+#else
+   for (p=rx->JobIds; get_next_jobid_from_list(&p, &JobId) > 0; ) {
+      char ed1[50];
 
-      if (!nbsr) {                   /* Must add new JobId */
-        /* Add new JobId at end of chain */
-        for (nbsr=bsr; nbsr->next; nbsr=nbsr->next) 
-           {  }
-        nbsr->next = new_bsr();
-        nbsr->next->JobId = JobId;
-        nbsr->next->fi = new_findex();
-        nbsr->next->fi->findex = findex;
-        nbsr->next->fi->findex2 = findex;
-        return;
+      if (JobId == last_JobId) {
+         continue;                    /* eliminate duplicate JobIds */
+      }
+      last_JobId = JobId;
+      /*
+       * Find files for this JobId and insert them in the tree
+       */
+      Mmsg(rx->query, uar_sel_files, edit_int64(JobId, ed1));
+      if (!db_sql_query(ua->db, rx->query, insert_tree_handler, (void *)&tree)) {
+         ua->error_msg("%s", db_strerror(ua->db));
       }
    }
-
+#endif
    /* 
-    * At this point, bsr points to bsr containing JobId,
-    *  and we are sure that there is at least one fi record.
+    * At this point, the tree is built, so we can garbage collect
+    * any memory released by the SQL engine that RedHat has 
+    * not returned to the OS :-( 
+    */
+    garbage_collect_memory();
+
+   /*
+    * Look at the first JobId on the list (presumably the oldest) and
+    *  if it is marked purged, don't do the manual selection because
+    *  the Job was pruned, so the tree is incomplete.
     */
-   lfi = fi = bsr->fi;
-   /* Check if this findex is smaller than first item */
-   if (findex < fi->findex) {
-      if ((findex+1) == fi->findex) {
-        fi->findex = findex;         /* extend down */
-        return;
+   if (tree.FileCount != 0) {
+      /* Find out if any Job is purged */
+      Mmsg(rx->query, "SELECT SUM(PurgedFiles) FROM Job WHERE JobId IN (%s)", rx->JobIds);
+      if (!db_sql_query(ua->db, rx->query, restore_count_handler, (void *)rx)) {
+         ua->error_msg("%s\n", db_strerror(ua->db));
       }
-      fi = new_findex();             /* yes, insert before first item */
-      fi->findex = findex;
-      fi->findex2 = findex;
-      fi->next = lfi;
-      bsr->fi = fi;
-      return;
-   }
-   /* Walk down fi chain and find where to insert insert new FileIndex */
-   for ( ; fi; fi=fi->next) {
-      if (findex == (fi->findex2 + 1)) {  /* extend up */
-        RBSR_FINDEX *nfi;     
-        fi->findex2 = findex;
-        if (fi->next && ((findex+1) == fi->next->findex)) { 
-           nfi = fi->next;
-           fi->findex2 = nfi->findex2;
-           fi->next = nfi->next;
-           free(nfi);
-        }
-        return;
+      /* rx->JobId is the PurgedFiles flag */
+      if (rx->found && rx->JobId > 0) {
+         tree.FileCount = 0;           /* set count to zero, no tree selection */
       }
-      if (findex < fi->findex) {      /* add before */
-        if ((findex+1) == fi->findex) {
-           fi->findex = findex;
-           return;
-        }
-        break;
+   }
+   if (tree.FileCount == 0) {
+      OK = ask_for_fileregex(ua, rx);
+      if (OK) {
+         last_JobId = 0;
+         for (p=rx->JobIds; get_next_jobid_from_list(&p, &JobId) > 0; ) {
+             if (JobId == last_JobId) {
+                continue;                    /* eliminate duplicate JobIds */
+             }
+             add_findex_all(rx->bsr, JobId);
+         }
       }
-      lfi = fi;
-   }
-   /* Add to last place found */
-   fi = new_findex();
-   fi->findex = findex;
-   fi->findex2 = findex;
-   fi->next = lfi->next;
-   lfi->next = fi;
-   return;
-}
-
-/*
- * This callback routine is responsible for inserting the
- *  items it gets into the directory tree. For each JobId selected
- *  this routine is called once for each file. We do not allow
- *  duplicate filenames, but instead keep the info from the most
- *  recent file entered (i.e. the JobIds are assumed to be sorted)
- */
-static int insert_tree_handler(void *ctx, int num_fields, char **row)
-{
-   TREE_CTX *tree = (TREE_CTX *)ctx;
-   char fname[2000];
-   TREE_NODE *node, *new_node;
-   int type;
-
-   strip_trailing_junk(row[1]);
-   if (*row[1] == 0) {
-      if (*row[0] != '/') {           /* Must be Win32 directory */
-        type = TN_DIR_NLS;
+   } else {
+      char ec1[50];
+      if (tree.all) {
+         ua->info_msg(_("\n%s files inserted into the tree and marked for extraction.\n"),
+                      edit_uint64_with_commas(tree.FileCount, ec1));
       } else {
-        type = TN_DIR;
+         ua->info_msg(_("\n%s files inserted into the tree.\n"),
+                      edit_uint64_with_commas(tree.FileCount, ec1));
       }
-   } else {
-      type = TN_FILE;
-   }
-   sprintf(fname, "%s%s", row[0], row[1]);
-   if (tree->avail_node) {
-      node = tree->avail_node;
-   } else {
-      node = new_tree_node(tree->root, type);
-      tree->avail_node = node;
-   }
-   Dmsg3(200, "FI=%d type=%d fname=%s\n", node->FileIndex, type, fname);
-   new_node = insert_tree_node(fname, node, tree->root, NULL);
-   /* Note, if node already exists, save new one for next time */
-   if (new_node != node) {
-      tree->avail_node = node;
-   } else {
-      tree->avail_node = NULL;
-   }
-   new_node->FileIndex = atoi(row[2]);
-   new_node->JobId = atoi(row[3]);
-   new_node->type = type;
-   new_node->extract = 1;            /* extract all by default */
-   tree->cnt++;
-   return 0;
-}
-
 
-/*
- * Set extract to value passed. We recursively walk
- *  down the tree setting all children if the 
- *  node is a directory.
- */
-static void set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, int value)
-{
-   TREE_NODE *n;
-   FILE_DBR fdbr;
-   struct stat statp;
-
-   node->extract = value;
-   /* For a non-file (i.e. directory), we see all the children */
-   if (node->type != TN_FILE) {
-      for (n=node->child; n; n=n->sibling) {
-        set_extract(ua, n, tree, value);
+      if (find_arg(ua, NT_("done")) < 0) {
+         /* Let the user interact in selecting which files to restore */
+         OK = user_select_files_from_tree(&tree);
       }
-   } else if (value) {
-      char cwd[2000];
-      /* Ordinary file, we get the full path, look up the
-       * attributes, decode them, and if we are hard linked to
-       * a file that was saved, we must load that file too.
+
+      /*
+       * Walk down through the tree finding all files marked to be
+       *  extracted making a bootstrap file.
        */
-      tree_getpath(node, cwd, sizeof(cwd));
-      fdbr.FileId = 0;
-      fdbr.JobId = node->JobId;
-      if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, &fdbr)) {
-        uint32_t LinkFI;
-        decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
-        /*
-         * If we point to a hard linked file, traverse the tree to
-         * find that file, and mark it for restoration as well. It
-         * must have the Link we just obtained and the same JobId.
-         */
-        if (LinkFI) {
-           for (n=first_tree_node(tree->root); n; n=next_tree_node(n)) {
-              if (n->FileIndex == LinkFI && n->JobId == node->JobId) {
-                 n->extract = 1;
-                 break;
-              }
-           }
-        }
+      if (OK) {
+         for (TREE_NODE *node=first_tree_node(tree.root); node; node=next_tree_node(node)) {
+            Dmsg2(400, "FI=%d node=0x%x\n", node->FileIndex, node);
+            if (node->extract || node->extract_dir) {
+               Dmsg3(400, "JobId=%lld type=%d FI=%d\n", (uint64_t)node->JobId, node->type, node->FileIndex);
+               /* TODO: optimize bsr insertion when jobid are non sorted */
+               add_delta_list_findex(rx, node->delta_list);
+               add_findex(rx->bsr, node->JobId, node->FileIndex);
+               if (node->extract && node->type != TN_NEWDIR) {
+                  rx->selected_files++;  /* count only saved files */
+               }
+            }
+         }
       }
    }
+
+   free_tree(tree.root);              /* free the directory tree */
+   return OK;
 }
 
-static int markcmd(UAContext *ua, TREE_CTX *tree)
+
+/*
+ * This routine is used to get the current backup or a backup
+ *   before the specified date.
+ */
+static bool select_backups_before_date(UAContext *ua, RESTORE_CTX *rx, char *date)
 {
-   TREE_NODE *node;
+   bool ok = false;
+   FILESET_DBR fsr;
+   CLIENT_DBR cr;
+   char fileset_name[MAX_NAME_LENGTH];
+   char ed1[50], ed2[50];
+   char pool_select[MAX_NAME_LENGTH];
+   int i;
 
-   if (ua->argc < 2)
-      return 1;
-   if (!tree->node->child) {    
-      return 1;
+   /* Create temp tables */
+   db_sql_query(ua->db, uar_del_temp, NULL, NULL);
+   db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
+   if (!db_sql_query(ua->db, uar_create_temp[db_get_type_index(ua->db)], NULL, NULL)) {
+      ua->error_msg("%s\n", db_strerror(ua->db));
    }
-   for (node = tree->node->child; node; node=node->sibling) {
-      if (fnmatch(ua->argk[1], node->fname, 0) == 0) {
-        set_extract(ua, node, tree, 1);
-      }
+   if (!db_sql_query(ua->db, uar_create_temp1[db_get_type_index(ua->db)], NULL, NULL)) {
+      ua->error_msg("%s\n", db_strerror(ua->db));
    }
-   return 1;
-}
+   /*
+    * Select Client from the Catalog
+    */
+   memset(&cr, 0, sizeof(cr));
+   if (!get_client_dbr(ua, &cr)) {
+      goto bail_out;
+   }
+   bstrncpy(rx->ClientName, cr.Name, sizeof(rx->ClientName));
 
-static int countcmd(UAContext *ua, TREE_CTX *tree)
-{
-   int total, extract;
-
-   total = extract = 0;
-   for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
-      if (node->type != TN_NEWDIR) {
-        total++;
-        if (node->extract) {
-           extract++;
-        }
+   /*
+    * Get FileSet
+    */
+   memset(&fsr, 0, sizeof(fsr));
+   i = find_arg_with_value(ua, "FileSet");
+
+   if (i >= 0 && is_name_valid(ua->argv[i], &ua->errmsg)) {
+      bstrncpy(fsr.FileSet, ua->argv[i], sizeof(fsr.FileSet));
+      if (!db_get_fileset_record(ua->jcr, ua->db, &fsr)) {
+         ua->error_msg(_("Error getting FileSet \"%s\": ERR=%s\n"), fsr.FileSet,
+            db_strerror(ua->db));
+         i = -1;
       }
+   } else if (i >= 0) {         /* name is invalid */
+      ua->error_msg(_("FileSet argument: %s\n"), ua->errmsg);
    }
-   bsendmsg(ua, "%d total files. %d marked for restoration.\n", total, extract);
-   return 1;
-}
 
-static int findcmd(UAContext *ua, TREE_CTX *tree)
-{
-   char cwd[2000];
+   if (i < 0) {                       /* fileset not found */
+      edit_int64(cr.ClientId, ed1);
+      Mmsg(rx->query, uar_sel_fileset, ed1, ed1);
+      start_prompt(ua, _("The defined FileSet resources are:\n"));
+      if (!db_sql_query(ua->db, rx->query, fileset_handler, (void *)ua)) {
+         ua->error_msg("%s\n", db_strerror(ua->db));
+      }
+      if (do_prompt(ua, _("FileSet"), _("Select FileSet resource"),
+                 fileset_name, sizeof(fileset_name)) < 0) {
+         ua->error_msg(_("No FileSet found for client \"%s\".\n"), cr.Name);
+         goto bail_out;
+      }
 
-   if (ua->argc == 1) {
-      bsendmsg(ua, _("No file specification given.\n"));
-      return 0;
-   }
-   
-   for (int i=1; i < ua->argc; i++) {
-      for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
-        if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
-           tree_getpath(node, cwd, sizeof(cwd));
-            bsendmsg(ua, "%s%s\n", node->extract?"*":"", cwd);
-        }
+      bstrncpy(fsr.FileSet, fileset_name, sizeof(fsr.FileSet));
+      if (!db_get_fileset_record(ua->jcr, ua->db, &fsr)) {
+         ua->warning_msg(_("Error getting FileSet record: %s\n"), db_strerror(ua->db));
+         ua->send_msg(_("This probably means you modified the FileSet.\n"
+                     "Continuing anyway.\n"));
       }
    }
-   return 1;
-}
 
+   /* If Pool specified, add PoolId specification */
+   pool_select[0] = 0;
+   if (rx->pool) {
+      POOL_DBR pr;
+      memset(&pr, 0, sizeof(pr));
+      bstrncpy(pr.Name, rx->pool->name(), sizeof(pr.Name));
+      if (db_get_pool_record(ua->jcr, ua->db, &pr)) {
+         bsnprintf(pool_select, sizeof(pool_select), "AND Media.PoolId=%s ", 
+            edit_int64(pr.PoolId, ed1));
+      } else {
+         ua->warning_msg(_("Pool \"%s\" not found, using any pool.\n"), pr.Name);
+      }
+   }
 
+   /* Find JobId of last Full backup for this client, fileset */
+   edit_int64(cr.ClientId, ed1);
+   Mmsg(rx->query, uar_last_full, ed1, ed1, date, fsr.FileSet,
+         pool_select);
+   if (!db_sql_query(ua->db, rx->query, NULL, NULL)) {
+      ua->error_msg("%s\n", db_strerror(ua->db));
+      goto bail_out;
+   }
 
-static int lscmd(UAContext *ua, TREE_CTX *tree)
-{
-   TREE_NODE *node;
+   /* Find all Volumes used by that JobId */
+   if (!db_sql_query(ua->db, uar_full, NULL, NULL)) {
+      ua->error_msg("%s\n", db_strerror(ua->db));
+      goto bail_out;
+   }
 
-   if (!tree->node->child) {    
-      return 1;
+   /* Note, this is needed because I don't seem to get the callback
+    * from the call just above.
+    */
+   rx->JobTDate = 0;
+   if (!db_sql_query(ua->db, uar_sel_all_temp1, last_full_handler, (void *)rx)) {
+      ua->warning_msg("%s\n", db_strerror(ua->db));
    }
-   for (node = tree->node->child; node; node=node->sibling) {
-      if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
-         bsendmsg(ua, "%s%s%s\n", node->extract?"*":"", node->fname,
-            (node->type==TN_DIR||node->type==TN_NEWDIR)?"/":"");
-      }
+   if (rx->JobTDate == 0) {
+      ua->error_msg(_("No Full backup before %s found.\n"), date);
+      goto bail_out;
    }
-   return 1;
-}
-
-extern char *getuser(uid_t uid);
-extern char *getgroup(gid_t gid);
 
-/*
- * This is actually the long form used for "dir"
- */
-static void ls_output(char *buf, char *fname, int extract, struct stat *statp)
-{
-   char *p, *f;
-   char ec1[30];
-   int n;
-
-   p = encode_mode(statp->st_mode, buf);
-   n = sprintf(p, "  %2d ", (uint32_t)statp->st_nlink);
-   p += n;
-   n = sprintf(p, "%-8.8s %-8.8s", getuser(statp->st_uid), getgroup(statp->st_gid));
-   p += n;
-   n = sprintf(p, "%8.8s  ", edit_uint64(statp->st_size, ec1));
-   p += n;
-   p = encode_time(statp->st_ctime, p);
-   *p++ = ' ';
-   if (extract) {
-      *p++ = '*';
-   } else {
-      *p++ = ' ';
+   /* Now find most recent Differental Job after Full save, if any */
+   Mmsg(rx->query, uar_dif, edit_uint64(rx->JobTDate, ed1), date,
+        edit_int64(cr.ClientId, ed2), fsr.FileSet, pool_select);
+   if (!db_sql_query(ua->db, rx->query, NULL, NULL)) {
+      ua->warning_msg("%s\n", db_strerror(ua->db));
+   }
+   /* Now update JobTDate to look into Differental, if any */
+   rx->JobTDate = 0;
+   if (!db_sql_query(ua->db, uar_sel_all_temp, last_full_handler, (void *)rx)) {
+      ua->warning_msg("%s\n", db_strerror(ua->db));
+   }
+   if (rx->JobTDate == 0) {
+      ua->error_msg(_("No Full backup before %s found.\n"), date);
+      goto bail_out;
    }
-   for (f=fname; *f; )
-      *p++ = *f++;
-   *p = 0;
-}
 
+   /* Now find all Incremental Jobs after Full/dif save */
+   Mmsg(rx->query, uar_inc, edit_uint64(rx->JobTDate, ed1), date,
+        edit_int64(cr.ClientId, ed2), fsr.FileSet, pool_select);
+   if (!db_sql_query(ua->db, rx->query, NULL, NULL)) {
+      ua->warning_msg("%s\n", db_strerror(ua->db));
+   }
 
-/*
- * Like ls command, but give more detail on each file
- */
-static int dircmd(UAContext *ua, TREE_CTX *tree)
-{
-   TREE_NODE *node;
-   FILE_DBR fdbr;
-   struct stat statp;
-   char buf[1000];
-   char cwd[1100];
+   /* Get the JobIds from that list */
+   rx->last_jobid[0] = rx->JobIds[0] = 0;
 
-   if (!tree->node->child) {    
-      return 1;
+   if (!db_sql_query(ua->db, uar_sel_jobid_temp, jobid_handler, (void *)rx)) {
+      ua->warning_msg("%s\n", db_strerror(ua->db));
    }
-   for (node = tree->node->child; node; node=node->sibling) {
-      if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
-        tree_getpath(node, cwd, sizeof(cwd));
-        fdbr.FileId = 0;
-        fdbr.JobId = node->JobId;
-        if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, &fdbr)) {
-           uint32_t LinkFI;
-           decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
-           ls_output(buf, cwd, node->extract, &statp);
-            bsendmsg(ua, "%s\n", buf);
-        } else {
-           /* Something went wrong getting attributes -- print name */
-            bsendmsg(ua, "%s%s%s\n", node->extract?"*":"", node->fname,
-               (node->type==TN_DIR||node->type==TN_NEWDIR)?"/":"");
-        }
+
+   if (rx->JobIds[0] != 0) {
+      if (find_arg(ua, NT_("copies")) > 0) {
+         /* Display a list of all copies */
+         db_list_copies_records(ua->jcr, ua->db, 0, rx->JobIds, 
+                                prtit, ua, HORZ_LIST);
       }
+      /* Display a list of Jobs selected for this restore */
+      db_list_sql_query(ua->jcr, ua->db, uar_list_temp, prtit, ua, 1,HORZ_LIST);
+      ok = true;
+
+   } else {
+      ua->warning_msg(_("No jobs found.\n"));
    }
-   return 1;
-}
 
+bail_out:
+   db_sql_query(ua->db, uar_del_temp, NULL, NULL);
+   db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
+   return ok;
+}
 
-static int helpcmd(UAContext *ua, TREE_CTX *tree) 
+static int restore_count_handler(void *ctx, int num_fields, char **row)
 {
-   unsigned int i;
-
-/* usage(); */
-   bsendmsg(ua, _("  Command    Description\n  =======    ===========\n"));
-   for (i=0; i<comsize; i++) {
-      bsendmsg(ua, _("  %-10s %s\n"), _(commands[i].key), _(commands[i].help));
-   }
-   bsendmsg(ua, "\n");
-   return 1;
+   RESTORE_CTX *rx = (RESTORE_CTX *)ctx;
+   rx->JobId = str_to_int64(row[0]);
+   rx->found = true;
+   return 0;
 }
 
 /*
- * Change directories. Note, if the user specifies x: and it fails,
- *   we assume it is a Win32 absolute cd rather than relative and
- *   try a second time with /x: ...  Win32 kludge.
+ * Callback handler to get JobId and FileIndex for files
+ *   can insert more than one depending on the caller.
  */
-static int cdcmd(UAContext *ua, TREE_CTX *tree) 
+static int jobid_fileindex_handler(void *ctx, int num_fields, char **row)
 {
-   TREE_NODE *node;
-   char cwd[2000];
+   RESTORE_CTX *rx = (RESTORE_CTX *)ctx;
 
-   if (ua->argc != 2) {
-      return 1;
-   }
-   node = tree_cwd(ua->argk[1], tree->root, tree->node);
-   if (!node) {
-      /* Try once more if Win32 drive -- make absolute */
-      if (ua->argk[1][1] == ':') {  /* win32 drive */
-         strcpy(cwd, "/");
-        strcat(cwd, ua->argk[1]);
-        node = tree_cwd(cwd, tree->root, tree->node);
-      }
-      if (!node) {
-         bsendmsg(ua, _("Invalid path given.\n"));
-      } else {
-        tree->node = node;
-      }
-   } else {
-      tree->node = node;
-   }
-   tree_getpath(tree->node, cwd, sizeof(cwd));
-   bsendmsg(ua, _("cwd is: %s\n"), cwd);
-   return 1;
-}
-
-static int pwdcmd(UAContext *ua, TREE_CTX *tree) 
-{
-   char cwd[2000];
-   tree_getpath(tree->node, cwd, sizeof(cwd));
-   bsendmsg(ua, _("cwd is: %s\n"), cwd);
-   return 1;
+   Dmsg2(200, "JobId=%s FileIndex=%s\n", row[0], row[1]);
+   rx->JobId = str_to_int64(row[0]);
+   add_findex(rx->bsr, rx->JobId, str_to_int64(row[1]));
+   rx->found = true;
+   rx->selected_files++;
+   return 0;
 }
 
-
-static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
+/*
+ * Callback handler make list of JobIds
+ */
+static int jobid_handler(void *ctx, int num_fields, char **row)
 {
-   TREE_NODE *node;
+   RESTORE_CTX *rx = (RESTORE_CTX *)ctx;
 
-   if (ua->argc < 2)
-      return 1;
-   if (!tree->node->child) {    
-      return 1;
+   if (strcmp(rx->last_jobid, row[0]) == 0) {
+      return 0;                       /* duplicate id */
    }
-   for (node = tree->node->child; node; node=node->sibling) {
-      if (fnmatch(ua->argk[1], node->fname, 0) == 0) {
-        set_extract(ua, node, tree, 0);
-      }
+   bstrncpy(rx->last_jobid, row[0], sizeof(rx->last_jobid));
+   if (rx->JobIds[0] != 0) {
+      pm_strcat(rx->JobIds, ",");
    }
-   return 1;
-}
-
-static int quitcmd(UAContext *ua, TREE_CTX *tree) 
-{
+   pm_strcat(rx->JobIds, row[0]);
    return 0;
 }
 
 
 /*
- * Called here with each name to be added to the list. The name is
- *   added to the list if it is not already in the list.
+ * Callback handler to pickup last Full backup JobTDate
  */
-static int unique_name_list_handler(void *ctx, int num_fields, char **row)
+static int last_full_handler(void *ctx, int num_fields, char **row)
 {
-   NAME_LIST *name = (NAME_LIST *)ctx;
+   RESTORE_CTX *rx = (RESTORE_CTX *)ctx;
 
-   if (name->num_ids == MAX_ID_LIST_LEN) {  
-      return 1;
-   }
-   if (name->num_ids == name->max_ids) {
-      if (name->max_ids == 0) {
-        name->max_ids = 1000;
-        name->name = (char **)bmalloc(sizeof(char *) * name->max_ids);
-      } else {
-        name->max_ids = (name->max_ids * 3) / 2;
-        name->name = (char **)brealloc(name->name, sizeof(char *) * name->max_ids);
-      }
-   }
-   for (int i=0; i<name->num_ids; i++) {
-      if (strcmp(name->name[i], row[0]) == 0) {
-        return 0;                    /* already in list, return */
-      }
-   }
-   /* Add new name to list */
-   name->name[name->num_ids++] = bstrdup(row[0]);
+   rx->JobTDate = str_to_int64(row[1]);
    return 0;
 }
 
-
 /*
- * Print names in the list
+ * Callback handler build FileSet name prompt list
  */
-static void print_name_list(UAContext *ua, NAME_LIST *name_list)
-{ 
-   int i;
-
-   for (i=0; i < name_list->num_ids; i++) {
-      bsendmsg(ua, "%s\n", name_list->name[i]);
+static int fileset_handler(void *ctx, int num_fields, char **row)
+{
+   /* row[0] = FileSet (name) */
+   if (row[0]) {
+      add_prompt((UAContext *)ctx, row[0]);
    }
+   return 0;
 }
 
-
 /*
  * Free names in the list
  */
 static void free_name_list(NAME_LIST *name_list)
-{ 
-   int i;
-
-   for (i=0; i < name_list->num_ids; i++) {
+{
+   for (int i=0; i < name_list->num_ids; i++) {
       free(name_list->name[i]);
    }
-   if (name_list->name) {
-      free(name_list->name);
-   }
+   bfree_and_null(name_list->name);
    name_list->max_ids = 0;
    name_list->num_ids = 0;
 }
 
-static void get_storage_from_mediatype(UAContext *ua, NAME_LIST *name_list, JobIds *ji)
+void find_storage_resource(UAContext *ua, RESTORE_CTX &rx, char *Storage, char *MediaType) 
 {
-   char name[MAX_NAME_LENGTH];
-   STORE *store = NULL;
-
-   if (name_list->num_ids > 1) {
-      bsendmsg(ua, _("Warning, the JobIds that you selected refer to more than one MediaType.\n"
-         "Restore is not possible. The MediaTypes used are:\n"));
-      print_name_list(ua, name_list);
-      ji->store = select_storage_resource(ua);
+   STORE *store;
+
+   if (rx.store) {
+      Dmsg1(200, "Already have store=%s\n", rx.store->name());
       return;
    }
+   /*
+    * Try looking up Storage by name
+    */
+   LockRes();
+   foreach_res(store, R_STORAGE) {
+      if (strcmp(Storage, store->name()) == 0) {
+         if (acl_access_ok(ua, Storage_ACL, store->name())) {
+            rx.store = store;
+         }
+         break;
+      }
+   }
+   UnlockRes();
 
-   if (name_list->num_ids == 0) {
-      bsendmsg(ua, _("No MediaType found for your JobIds.\n"));
-      ji->store = select_storage_resource(ua);
+   if (rx.store) {
+      /* Check if an explicit storage resource is given */
+      store = NULL;
+      int i = find_arg_with_value(ua, "storage");
+      if (i > 0) {
+         store = (STORE *)GetResWithName(R_STORAGE, ua->argv[i]);
+         if (store && !acl_access_ok(ua, Storage_ACL, store->name())) {
+            store = NULL;
+         }
+      }
+      if (store && (store != rx.store)) {
+         ua->info_msg(_("Warning default storage overridden by \"%s\" on command line.\n"),
+            store->name());
+         rx.store = store;
+         Dmsg1(200, "Set store=%s\n", rx.store->name());
+      }
       return;
    }
 
-   start_prompt(ua, _("The defined Storage resources are:\n"));
-   LockRes();
-   while ((store = (STORE *)GetNextRes(R_STORAGE, (RES *)store))) {
-      if (strcmp(store->media_type, name_list->name[0]) == 0) {
-        add_prompt(ua, store->hdr.name);
+   /* If no storage resource, try to find one from MediaType */
+   if (!rx.store) {
+      LockRes();
+      foreach_res(store, R_STORAGE) {
+         if (strcmp(MediaType, store->media_type) == 0) {
+            if (acl_access_ok(ua, Storage_ACL, store->name())) {
+               rx.store = store;
+               Dmsg1(200, "Set store=%s\n", rx.store->name());
+               if (Storage == NULL) {
+                  ua->warning_msg(_("Using Storage \"%s\" from MediaType \"%s\".\n"),
+                     store->name(), MediaType);
+               } else {
+                  ua->warning_msg(_("Storage \"%s\" not found, using Storage \"%s\" from MediaType \"%s\".\n"),
+                     Storage, store->name(), MediaType);
+               }
+            }
+            UnlockRes();
+            return;
+         }
       }
+      UnlockRes();
+      ua->warning_msg(_("\nUnable to find Storage resource for\n"
+         "MediaType \"%s\", needed by the Jobs you selected.\n"), MediaType);
    }
-   UnlockRes();
-   do_prompt(ua, _("Select Storage resource"), name, sizeof(name));
-   ji->store = (STORE *)GetResWithName(R_STORAGE, name);
-   if (!ji->store) {
-      bsendmsg(ua, _("\nWarning. Unable to find Storage resource for\n"
-         "MediaType %s, needed by the Jobs you selected.\n"
-         "You will be allowed to select a Storage device later.\n"),
-        name_list->name[0]); 
+
+   /* Take command line arg, or ask user if none */
+   rx.store = get_storage_resource(ua, false /* don't use default */);
+   if (rx.store) {
+      Dmsg1(200, "Set store=%s\n", rx.store->name());
    }
+
 }