]> git.sur5r.net Git - bacula/bacula/blobdiff - bacula/src/dird/ua_tree.c
regress: add new many-reload-test to standard scripts
[bacula/bacula] / bacula / src / dird / ua_tree.c
index ed999b7991ca9ada15aa6abfb746a73b961c8814..a03eb1b15845144e390f2850923317bfe800c894 100644 (file)
@@ -1,26 +1,27 @@
 /*
- *
+   Bacula(R) - The Network Backup Solution
+
+   Copyright (C) 2000-2017 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 -- User Agent Database File tree for Restore
  *      command. This file interacts with the user implementing the
  *      UA tree commands.
  *
  *     Kern Sibbald, July MMII
- *
- *   Version $Id$
- */
-/*
-   Copyright (C) 2002-2005 Kern Sibbald
-
-   This program is free software; you can redistribute it and/or
-   modify it under the terms of the GNU General Public License
-   version 2 as amended with additional clauses defined in the
-   file LICENSE in the main source directory.
-
-   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 
-   the file LICENSE for additional details.
-
  */
 
 #include "bacula.h"
@@ -42,38 +43,50 @@ static int findcmd(UAContext *ua, TREE_CTX *tree);
 static int lscmd(UAContext *ua, TREE_CTX *tree);
 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree);
 static int dircmd(UAContext *ua, TREE_CTX *tree);
+static int dot_dircmd(UAContext *ua, TREE_CTX *tree);
 static int estimatecmd(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 dot_pwdcmd(UAContext *ua, TREE_CTX *tree);
 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree);
 static int quitcmd(UAContext *ua, TREE_CTX *tree);
 static int donecmd(UAContext *ua, TREE_CTX *tree);
-
+static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree);
+static int dot_lscmd(UAContext *ua, TREE_CTX *tree);
+static int dot_helpcmd(UAContext *ua, TREE_CTX *tree);
+static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree);
 
 struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
 static struct cmdstruct commands[] = {
- { N_("cd"),         cdcmd,        _("change current directory")},
- { N_("count"),      countcmd,     _("count marked files in and below the cd")},
- { N_("dir"),        dircmd,       _("long list current directory, wildcards allowed")},
- { N_("done"),       donecmd,      _("leave file selection mode")},
- { N_("estimate"),   estimatecmd,  _("estimate restore size")},
- { N_("exit"),       donecmd,      _("same as done command")},
- { N_("find"),       findcmd,      _("find files, wildcards allowed")},
- { N_("help"),       helpcmd,      _("print help")},
- { N_("ls"),         lscmd,        _("list current directory, wildcards allowed")},
- { N_("lsmark"),     lsmarkcmd,    _("list the marked files in and below the cd")},
- { N_("mark"),       markcmd,      _("mark dir/file to be restored recursively in dirs")},
- { N_("markdir"),    markdircmd,   _("mark directory name to be restored (no files)")},
- { N_("pwd"),        pwdcmd,       _("print current working directory")},
- { N_("unmark"),     unmarkcmd,    _("unmark dir/file to be restored recursively in dir")},
- { N_("unmarkdir"),  unmarkdircmd, _("unmark directory name only no recursion")},
- { N_("quit"),       quitcmd,      _("quit and do not do restore")},
- { N_("?"),          helpcmd,      _("print help")},
+ { NT_("add"),        markcmd,      _("add dir/file to be restored recursively, wildcards allowed")},
+ { NT_("cd"),         cdcmd,        _("change current directory")},
+ { NT_("count"),      countcmd,     _("count marked files in and below the cd")},
+ { NT_("delete"),     unmarkcmd,    _("delete dir/file to be restored recursively in dir")},
+ { NT_("dir"),        dircmd,       _("long list current directory, wildcards allowed")},
+ { NT_(".dir"),       dot_dircmd,   _("long list current directory, wildcards allowed")},
+ { NT_("done"),       donecmd,      _("leave file selection mode")},
+ { NT_("estimate"),   estimatecmd,  _("estimate restore size")},
+ { NT_("exit"),       donecmd,      _("same as done command")},
+ { NT_("find"),       findcmd,      _("find files, wildcards allowed")},
+ { NT_("help"),       helpcmd,      _("print help")},
+ { NT_("ls"),         lscmd,        _("list current directory, wildcards allowed")},
+ { NT_(".ls"),        dot_lscmd,    _("list current directory, wildcards allowed")},
+ { NT_(".lsdir"),     dot_lsdircmd, _("list subdir in current directory, wildcards allowed")},
+ { NT_("lsmark"),     lsmarkcmd,    _("list the marked files in and below the cd")},
+ { NT_(".lsmark"),    dot_lsmarkcmd,_("list the marked files in")},
+ { NT_("mark"),       markcmd,      _("mark dir/file to be restored recursively, wildcards allowed")},
+ { NT_("markdir"),    markdircmd,   _("mark directory name to be restored (no files)")},
+ { NT_("pwd"),        pwdcmd,       _("print current working directory")},
+ { NT_(".pwd"),       dot_pwdcmd,   _("print current working directory")},
+ { NT_("unmark"),     unmarkcmd,    _("unmark dir/file to be restored recursively in dir")},
+ { NT_("unmarkdir"),  unmarkdircmd, _("unmark directory name only no recursion")},
+ { NT_("quit"),       quitcmd,      _("quit and do not do restore")},
+ { NT_(".help"),      dot_helpcmd,  _("print help")},
+ { NT_("?"),          helpcmd,      _("print help")},
              };
-#define comsize (sizeof(commands)/sizeof(struct cmdstruct))
-
+#define comsize ((int)(sizeof(commands)/sizeof(struct cmdstruct)))
 
 /*
  * Enter a prompt mode where the user can select/deselect
@@ -87,46 +100,59 @@ bool user_select_files_from_tree(TREE_CTX *tree)
    /* Get a new context so we don't destroy restore command args */
    UAContext *ua = new_ua_context(tree->ua->jcr);
    ua->UA_sock = tree->ua->UA_sock;   /* patch in UA socket */
+   ua->api = tree->ua->api;           /* keep API flag too */
+   BSOCK *user = ua->UA_sock;
 
-   bsendmsg(tree->ua, _(
+   ua->send_msg(_(
       "\nYou are now entering file selection mode where you add (mark) and\n"
       "remove (unmark) files to be restored. No files are initially added, unless\n"
       "you used the \"all\" keyword on the command line.\n"
       "Enter \"done\" to leave this mode.\n\n"));
+   if (ua->api) user->signal(BNET_START_RTREE);
    /*
     * 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);
+   ua->send_msg(_("cwd is: %s\n"), cwd);
    for ( ;; ) {
       int found, len, i;
-      if (!get_cmd(ua, "$ ")) {
+      if (!get_cmd(ua, "$ ", true)) {
          break;
       }
-      parse_ua_args(ua);
+      if (ua->api) user->signal(BNET_CMD_BEGIN);
+      parse_args_only(ua->cmd, &ua->args, &ua->argc, ua->argk, ua->argv, MAX_CMD_ARGS);
       if (ua->argc == 0) {
-         break;
+         ua->warning_msg(_("Invalid command \"%s\".  Enter \"done\" to exit.\n"), ua->cmd);
+         if (ua->api) user->signal(BNET_CMD_FAILED);
+         continue;
       }
 
       len = strlen(ua->argk[0]);
       found = 0;
       stat = false;
-      for (i=0; i<(int)comsize; i++)       /* search for command */
-         if (strncasecmp(ua->argk[0],  _(commands[i].key), len) == 0) {
+      for (i=0; i<comsize; i++)       /* search for command */
+         if (strncasecmp(ua->argk[0],  commands[i].key, len) == 0) {
             stat = (*commands[i].func)(ua, tree);   /* go execute command */
             found = 1;
             break;
          }
       if (!found) {
-         bsendmsg(tree->ua, _("Illegal command. Enter \"done\" to exit.\n"));
+         if (*ua->argk[0] == '.') {
+            /* Some unknow dot command -- probably .messages, ignore it */
+            continue;
+         }
+         ua->warning_msg(_("Invalid command \"%s\".  Enter \"done\" to exit.\n"), ua->cmd);
+         if (ua->api) user->signal(BNET_CMD_FAILED);
          continue;
       }
+      if (ua->api) user->signal(BNET_CMD_OK);
       if (!stat) {
          break;
       }
    }
+   if (ua->api) user->signal(BNET_END_RTREE);
    ua->UA_sock = NULL;                /* don't release restore socket */
    stat = !ua->quit;
    ua->quit = false;
@@ -134,7 +160,6 @@ bool user_select_files_from_tree(TREE_CTX *tree)
    return stat;
 }
 
-
 /*
  * This callback routine is responsible for inserting the
  *  items it gets into the directory tree. For each JobId selected
@@ -144,7 +169,7 @@ bool user_select_files_from_tree(TREE_CTX *tree)
  *
  *   See uar_sel_files in sql_cmds.c for query that calls us.
  *      row[0]=Path, row[1]=Filename, row[2]=FileIndex
- *      row[3]=JobId row[4]=LStat
+ *      row[3]=JobId row[4]=LStat row[5]=DeltaSeq
  */
 int insert_tree_handler(void *ctx, int num_fields, char **row)
 {
@@ -154,12 +179,15 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
    int type;
    bool hard_link, ok;
    int FileIndex;
+   int32_t delta_seq;
    JobId_t JobId;
+   HL_ENTRY *entry = NULL;
+   int32_t LinkFI;
 
-// Dmsg4(000, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
-//    row[2], row[3]);
-   if (*row[1] == 0) {                /* no filename => directory */
-      if (*row[0] != '/') {           /* Must be Win32 directory */
+   Dmsg4(150, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
+         row[2], row[3]);
+   if (*row[1] == 0) {                 /* no filename => directory */
+      if (!IsPathSeparator(*row[0])) { /* Must be Win32 directory */
          type = TN_DIR_NLS;
       } else {
          type = TN_DIR;
@@ -167,10 +195,37 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
    } else {
       type = TN_FILE;
    }
-   hard_link = (decode_LinkFI(row[4], &statp) != 0);
+   decode_stat(row[4], &statp, sizeof(statp), &LinkFI);
+
+   hard_link = (LinkFI != 0);
    node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
    JobId = str_to_int64(row[3]);
    FileIndex = str_to_int64(row[2]);
+   delta_seq = str_to_int64(row[5]);
+   Dmsg6(150, "node=0x%p JobId=%s FileIndex=%s Delta=%s node.delta=%d LinkFI=%d\n",
+         node, row[3], row[2], row[5], node->delta_seq, LinkFI);
+
+   /* TODO: check with hardlinks */
+   if (delta_seq > 0) {
+      if (delta_seq == (node->delta_seq + 1)) {
+         tree_add_delta_part(tree->root, node, node->JobId, node->FileIndex);
+
+      } else {
+         /* File looks to be deleted */
+         if (node->delta_seq == -1) { /* just created */
+            tree_remove_node(tree->root, node);
+
+         } else {
+            tree->ua->warning_msg(_("Something is wrong with the Delta sequence of %s, "
+                                    "skipping new parts. Current sequence is %d\n"),
+                                  row[1], node->delta_seq);
+
+            Dmsg3(0, "Something is wrong with Delta, skip it "
+                  "fname=%s d1=%d d2=%d\n", row[1], node->delta_seq, delta_seq);
+         }
+         return 0;
+      }
+   }
    /*
     * - The first time we see a file (node->inserted==true), we accept it.
     * - In the same JobId, we accept only the first copy of a
@@ -194,17 +249,40 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
       node->JobId = JobId;
       node->type = type;
       node->soft_link = S_ISLNK(statp.st_mode) != 0;
+      node->delta_seq = delta_seq;
+      node->can_access = true;
       if (tree->all) {
          node->extract = true;          /* extract all by default */
          if (type == TN_DIR || type == TN_DIR_NLS) {
             node->extract_dir = true;   /* if dir, extract it */
          }
       }
+      /* insert file having hardlinks into hardlink hashtable */
+      if (statp.st_nlink > 1 && type != TN_DIR && type != TN_DIR_NLS) {
+         if (!LinkFI) {
+            /* first occurrence - file hardlinked to */
+            entry = (HL_ENTRY *)tree->root->hardlinks.hash_malloc(sizeof(HL_ENTRY));
+            entry->key = (((uint64_t) JobId) << 32) + FileIndex;
+            entry->node = node;
+            tree->root->hardlinks.insert(entry->key, entry);
+         } else if (tree->hardlinks_in_mem) {
+            /* hardlink to known file index: lookup original file */
+            uint64_t file_key = (((uint64_t) JobId) << 32) + LinkFI;
+            HL_ENTRY *first_hl = (HL_ENTRY *) tree->root->hardlinks.lookup(file_key);
+            if (first_hl && first_hl->node) {
+               /* then add hardlink entry to linked node*/
+               entry = (HL_ENTRY *)tree->root->hardlinks.hash_malloc(sizeof(HL_ENTRY));
+               entry->key = (((uint64_t) JobId) << 32) + FileIndex;
+               entry->node = first_hl->node;
+               tree->root->hardlinks.insert(entry->key, entry);
+            }
+         }
+      }
    }
    if (node->inserted) {
       tree->FileCount++;
       if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
-         bsendmsg(tree->ua, "+");
+         tree->ua->send_msg("+");
          tree->LastCount = tree->FileCount;
       }
    }
@@ -212,7 +290,6 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
    return 0;
 }
 
-
 /*
  * Set extract to value passed. We recursively walk
  *  down the tree setting all children if the
@@ -221,8 +298,6 @@ int insert_tree_handler(void *ctx, int num_fields, char **row)
 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
 {
    TREE_NODE *n;
-   FILE_DBR fdbr;
-   struct stat statp;
    int count = 0;
 
    node->extract = extract;
@@ -242,46 +317,66 @@ static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extr
        * Walk up tree marking any unextracted parent to be
        * extracted.
        */
-      if (extract) {
+      if (!tree->no_auto_parent && extract) {
          while (node->parent && !node->parent->extract_dir) {
             node = node->parent;
             node->extract_dir = true;
          }
       }
    } else if (extract) {
-      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.
-       */
-      tree_getpath(node, cwd, sizeof(cwd));
-      fdbr.FileId = 0;
-      fdbr.JobId = node->JobId;
-      if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
-         int32_t LinkFI;
-         decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
+      uint64_t key = 0;
+      if (tree->hardlinks_in_mem) {
+         if (node->hard_link) {
+            key = (((uint64_t) node->JobId) << 32) + node->FileIndex;  /* every hardlink is in hashtable, and it points to linked file */
+         }
+      } else {
+         /* Get the hard link if it exists */
+         FILE_DBR fdbr;
+         struct stat statp;
+         char cwd[2000];
          /*
-          * If we point to a hard linked file, traverse the tree to
-          * find that file, and mark it to be restored as well. It
-          * must have the Link we just obtained and the same JobId.
+          * 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.
           */
-         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 = true;
-                  if (n->type == TN_DIR || n->type == TN_DIR_NLS) {
-                     n->extract_dir = true;
-                  }
-                  break;
-               }
-            }
+         tree_getpath(node, cwd, sizeof(cwd));
+         fdbr.FileId = 0;
+         fdbr.JobId = node->JobId;
+         if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
+            int32_t LinkFI;
+            decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
+            key = (((uint64_t) node->JobId) << 32) + LinkFI;  /* lookup by linked file's fileindex */
+         }
+      }
+      /* If file hard linked and we have a key */
+      if (node->hard_link && key != 0) {
+         /*
+          * If we point to a hard linked file, find that file in
+          * hardlinks hashmap, and mark it to be restored as well.
+          */
+         HL_ENTRY *entry = (HL_ENTRY *)tree->root->hardlinks.lookup(key);
+         if (entry && entry->node) {
+            n = entry->node;
+            n->extract = true;
+            n->extract_dir = (n->type == TN_DIR || n->type == TN_DIR_NLS);
          }
       }
    }
    return count;
 }
 
+static void strip_trailing_slash(char *arg)
+{
+   int len = strlen(arg);
+   if (len == 0) {
+      return;
+   }
+   len--;
+   if (arg[len] == '/') {       /* strip any trailing slash */
+      arg[len] = 0;
+   }
+}
+
 /*
  * Recursively mark the current directory to be restored as
  *  well as all directories and files below it.
@@ -293,10 +388,11 @@ static int markcmd(UAContext *ua, TREE_CTX *tree)
    char ec1[50];
 
    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
-      bsendmsg(ua, _("No files marked.\n"));
+      ua->send_msg(_("No files marked.\n"));
       return 1;
    }
    for (int i=1; i < ua->argc; i++) {
+      strip_trailing_slash(ua->argk[i]);
       foreach_child(node, tree->node) {
          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
             count += set_extract(ua, node, tree, true);
@@ -304,11 +400,11 @@ static int markcmd(UAContext *ua, TREE_CTX *tree)
       }
    }
    if (count == 0) {
-      bsendmsg(ua, _("No files marked.\n"));
+      ua->send_msg(_("No files marked.\n"));
    } else if (count == 1) {
-      bsendmsg(ua, _("1 file marked.\n"));
+      ua->send_msg(_("1 file marked.\n"));
    } else {
-      bsendmsg(ua, _("%s files marked.\n"),
+      ua->send_msg(_("%s files marked.\n"),
                edit_uint64_with_commas(count, ec1));
    }
    return 1;
@@ -321,10 +417,11 @@ static int markdircmd(UAContext *ua, TREE_CTX *tree)
    char ec1[50];
 
    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
-      bsendmsg(ua, _("No files marked.\n"));
+      ua->send_msg(_("No files marked.\n"));
       return 1;
    }
    for (int i=1; i < ua->argc; i++) {
+      strip_trailing_slash(ua->argk[i]);
       foreach_child(node, tree->node) {
          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
@@ -335,11 +432,11 @@ static int markdircmd(UAContext *ua, TREE_CTX *tree)
       }
    }
    if (count == 0) {
-      bsendmsg(ua, _("No directories marked.\n"));
+      ua->send_msg(_("No directories marked.\n"));
    } else if (count == 1) {
-      bsendmsg(ua, _("1 directory marked.\n"));
+      ua->send_msg(_("1 directory marked.\n"));
    } else {
-      bsendmsg(ua, _("%s directories marked.\n"),
+      ua->send_msg(_("%s directories marked.\n"),
                edit_uint64_with_commas(count, ec1));
    }
    return 1;
@@ -360,7 +457,7 @@ static int countcmd(UAContext *ua, TREE_CTX *tree)
          }
       }
    }
-   bsendmsg(ua, _("%s total files/dirs. %s marked to be restored.\n"),
+   ua->send_msg(_("%s total files/dirs. %s marked to be restored.\n"),
             edit_uint64_with_commas(total, ec1),
             edit_uint64_with_commas(num_extract, ec2));
    return 1;
@@ -371,8 +468,8 @@ static int findcmd(UAContext *ua, TREE_CTX *tree)
    char cwd[2000];
 
    if (ua->argc == 1) {
-      bsendmsg(ua, _("No file specification given.\n"));
-      return 0;
+      ua->send_msg(_("No file specification given.\n"));
+      return 1;      /* make it non-fatal */
    }
 
    for (int i=1; i < ua->argc; i++) {
@@ -387,14 +484,59 @@ static int findcmd(UAContext *ua, TREE_CTX *tree)
             } else {
                tag = "";
             }
-            bsendmsg(ua, "%s%s\n", tag, cwd);
+            ua->send_msg("%s%s\n", tag, cwd);
+         }
+      }
+   }
+   return 1;
+}
+
+static int dot_lsdircmd(UAContext *ua, TREE_CTX *tree)
+{
+   TREE_NODE *node;
+
+   if (!tree_node_has_child(tree->node)) {
+      return 1;
+   }
+
+   foreach_child(node, tree->node) {
+      if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
+         if (tree_node_has_child(node)) {
+            ua->send_msg("%s/\n", node->fname);
          }
       }
    }
+
    return 1;
 }
 
+static int dot_helpcmd(UAContext *ua, TREE_CTX *tree)
+{
+   for (int i=0; i<comsize; i++) {
+      /* List only non-dot commands */
+      if (commands[i].key[0] != '.') {
+         ua->send_msg("%s\n", commands[i].key);
+      }
+   }
+   return 1;
+}
+
+static int dot_lscmd(UAContext *ua, TREE_CTX *tree)
+{
+   TREE_NODE *node;
+
+   if (!tree_node_has_child(tree->node)) {
+      return 1;
+   }
 
+   foreach_child(node, tree->node) {
+      if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
+         ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
+      }
+   }
+
+   return 1;
+}
 
 static int lscmd(UAContext *ua, TREE_CTX *tree)
 {
@@ -413,7 +555,7 @@ static int lscmd(UAContext *ua, TREE_CTX *tree)
          } else {
             tag = "";
          }
-         bsendmsg(ua, "%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
+         ua->send_msg("%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
       }
    }
    return 1;
@@ -422,12 +564,40 @@ static int lscmd(UAContext *ua, TREE_CTX *tree)
 /*
  * Ls command that lists only the marked files
  */
-static void rlsmark(UAContext *ua, TREE_NODE *tnode)
+static int dot_lsmarkcmd(UAContext *ua, TREE_CTX *tree)
+{
+   TREE_NODE *node;
+   if (!tree_node_has_child(tree->node)) {
+      return 1;
+   }
+   foreach_child(node, tree->node) {
+      if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
+          (node->extract || node->extract_dir)) {
+         ua->send_msg("%s%s\n", node->fname, tree_node_has_child(node)?"/":"");
+      }
+   }
+   return 1;
+}
+
+/*
+ * This recursive ls command that lists only the marked files
+ */
+static void rlsmark(UAContext *ua, TREE_NODE *tnode, int level)
 {
    TREE_NODE *node;
+   const int max_level = 100;
+   char indent[max_level*2+1];
+   int i, j;
    if (!tree_node_has_child(tnode)) {
       return;
    }
+   level = MIN(level, max_level);
+   j = 0;
+   for (i=0; i<level; i++) {
+      indent[j++] = ' ';
+      indent[j++] = ' ';
+   }
+   indent[j] = 0;
    foreach_child(node, tnode) {
       if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
           (node->extract || node->extract_dir)) {
@@ -439,9 +609,9 @@ static void rlsmark(UAContext *ua, TREE_NODE *tnode)
          } else {
             tag = "";
          }
-         bsendmsg(ua, "%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
+         ua->send_msg("%s%s%s%s\n", indent, tag, node->fname, tree_node_has_child(node)?"/":"");
          if (tree_node_has_child(node)) {
-            rlsmark(ua, node);
+            rlsmark(ua, node, level+1);
          }
       }
    }
@@ -449,60 +619,81 @@ static void rlsmark(UAContext *ua, TREE_NODE *tnode)
 
 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
 {
-   rlsmark(ua, tree->node);
+   rlsmark(ua, tree->node, 0);
    return 1;
 }
 
-
-
-extern char *getuser(uid_t uid, char *name, int len);
-extern char *getgroup(gid_t gid, char *name, int len);
-
 /*
  * This is actually the long form used for "dir"
  */
-static void ls_output(char *buf, const char *fname, const char *tag, struct stat *statp)
+static void ls_output(guid_list *guid, char *buf, const char *fname, const char *tag,
+                      struct stat *statp, bool dot_cmd)
 {
    char *p;
    const char *f;
    char ec1[30];
    char en1[30], en2[30];
    int n;
+   time_t time;
 
    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, en1, sizeof(en1)),
-               getgroup(statp->st_gid, en2, sizeof(en2)));
-   p += n;
-   n = sprintf(p, "%10.10s  ", edit_uint64(statp->st_size, ec1));
-   p += n;
-   p = encode_time(statp->st_ctime, p);
-   *p++ = ' ';
-   *p++ = *tag;
+   if (dot_cmd) {
+      *p++ = ',';
+      n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
+      p += n;
+      n = sprintf(p, "%s,%s,",
+                  guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
+                  guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
+      p += n;
+      n = sprintf(p, "%s,", edit_int64(statp->st_size, ec1));
+      p += n;
+      p = encode_time(statp->st_mtime, p);
+      *p++ = ',';
+      *p++ = *tag;
+      *p++ = ',';
+   } else {
+      n = sprintf(p, "  %2d ", (uint32_t)statp->st_nlink);
+      p += n;
+      n = sprintf(p, "%-8.8s %-8.8s",
+                  guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
+                  guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
+      p += n;
+      n = sprintf(p, "%12.12s  ", edit_int64(statp->st_size, ec1));
+      p += n;
+      if (statp->st_ctime > statp->st_mtime) {
+         time = statp->st_ctime;
+      } else {
+         time = statp->st_mtime;
+      }
+      /* Display most recent time */
+      p = encode_time(time, p);
+      *p++ = ' ';
+      *p++ = *tag;
+   }
    for (f=fname; *f; ) {
       *p++ = *f++;
    }
    *p = 0;
 }
 
-
 /*
  * Like ls command, but give more detail on each file
  */
-static int dircmd(UAContext *ua, TREE_CTX *tree)
+static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
 {
    TREE_NODE *node;
    FILE_DBR fdbr;
    struct stat statp;
    char buf[1100];
    char cwd[1100], *pcwd;
+   guid_list *guid;
 
    if (!tree_node_has_child(tree->node)) {
-      bsendmsg(ua, _("Node %s has no children.\n"), tree->node->fname);
+      ua->send_msg(_("Node %s has no children.\n"), tree->node->fname);
       return 1;
    }
 
+   guid = new_guid_list();
    foreach_child(node, tree->node) {
       const char *tag;
       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
@@ -535,18 +726,29 @@ static int dircmd(UAContext *ua, TREE_CTX *tree)
          }
          if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
             int32_t LinkFI;
-            decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
+            decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
          } else {
             /* Something went wrong getting attributes -- print name */
             memset(&statp, 0, sizeof(statp));
          }
-         ls_output(buf, cwd, tag, &statp);
-         bsendmsg(ua, "%s\n", buf);
+         ls_output(guid, buf, cwd, tag, &statp, dot_cmd);
+         ua->send_msg("%s\n", buf);
       }
    }
+   free_guid_list(guid);
    return 1;
 }
 
+int dot_dircmd(UAContext *ua, TREE_CTX *tree)
+{
+   return do_dircmd(ua, tree, true/*dot command*/);
+}
+
+static int dircmd(UAContext *ua, TREE_CTX *tree)
+{
+   return do_dircmd(ua, tree, false/*not dot command*/);
+}
+
 
 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
 {
@@ -569,7 +771,7 @@ static int estimatecmd(UAContext *ua, TREE_CTX *tree)
             fdbr.JobId = node->JobId;
             if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
                int32_t LinkFI;
-               decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
+               decode_stat(fdbr.LStat, &statp, sizeof(statp), &LinkFI); /* decode stat pkt */
                if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
                   total_bytes += statp.st_size;
                }
@@ -580,7 +782,7 @@ static int estimatecmd(UAContext *ua, TREE_CTX *tree)
          }
       }
    }
-   bsendmsg(ua, _("%d total files; %d marked to be restored; %s bytes.\n"),
+   ua->send_msg(_("%d total files; %d marked to be restored; %s bytes.\n"),
             total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
    return 1;
 }
@@ -591,11 +793,14 @@ static int helpcmd(UAContext *ua, TREE_CTX *tree)
 {
    unsigned int i;
 
-   bsendmsg(ua, _("  Command    Description\n  =======    ===========\n"));
+   ua->send_msg(_("  Command    Description\n  =======    ===========\n"));
    for (i=0; i<comsize; i++) {
-      bsendmsg(ua, "  %-10s %s\n", _(commands[i].key), _(commands[i].help));
+      /* List only non-dot commands */
+      if (commands[i].key[0] != '.') {
+         ua->send_msg("  %-10s %s\n", _(commands[i].key), _(commands[i].help));
+      }
    }
-   bsendmsg(ua, "\n");
+   ua->send_msg("\n");
    return 1;
 }
 
@@ -609,10 +814,12 @@ static int cdcmd(UAContext *ua, TREE_CTX *tree)
    TREE_NODE *node;
    char cwd[2000];
 
+
    if (ua->argc != 2) {
+      ua->error_msg(_("Too few or too many arguments. Try using double quotes.\n"));
       return 1;
    }
-   strip_leading_space(ua->argk[1]);
+
    node = tree_cwd(ua->argk[1], tree->root, tree->node);
    if (!node) {
       /* Try once more if Win32 drive -- make absolute */
@@ -622,26 +829,38 @@ static int cdcmd(UAContext *ua, TREE_CTX *tree)
          node = tree_cwd(cwd, tree->root, tree->node);
       }
       if (!node) {
-         bsendmsg(ua, _("Invalid path given.\n"));
-      } else {
+         ua->warning_msg(_("Invalid path given.\n"));
+      }
+   }
+   if (node) {
+      if (node->can_access) {
          tree->node = node;
+      } else {
+         ua->warning_msg(_("Invalid path given. Permission denied.\n"));
       }
-   } else {
-      tree->node = node;
    }
-   tree_getpath(tree->node, cwd, sizeof(cwd));
-   bsendmsg(ua, _("cwd is: %s\n"), cwd);
-   return 1;
+   return pwdcmd(ua, tree);
 }
 
 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);
+   if (ua->api) {
+      ua->send_msg("%s", cwd);
+   } else {
+      ua->send_msg(_("cwd is: %s\n"), cwd);
+   }
    return 1;
 }
 
+static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree)
+{
+   char cwd[2000];
+   tree_getpath(tree->node, cwd, sizeof(cwd));
+   ua->send_msg("%s", cwd);
+   return 1;
+}
 
 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
 {
@@ -649,10 +868,11 @@ static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
    int count = 0;
 
    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
-      bsendmsg(ua, _("No files unmarked.\n"));
+      ua->send_msg(_("No files unmarked.\n"));
       return 1;
    }
    for (int i=1; i < ua->argc; i++) {
+      strip_trailing_slash(ua->argk[i]);
       foreach_child(node, tree->node) {
          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
             count += set_extract(ua, node, tree, false);
@@ -660,11 +880,12 @@ static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
       }
    }
    if (count == 0) {
-      bsendmsg(ua, _("No files unmarked.\n"));
+      ua->send_msg(_("No files unmarked.\n"));
    } else if (count == 1) {
-      bsendmsg(ua, _("1 file unmarked.\n"));
+      ua->send_msg(_("1 file unmarked.\n"));
    } else {
-      bsendmsg(ua, _("%d files unmarked.\n"), count);
+      char ed1[50];
+      ua->send_msg(_("%s files unmarked.\n"), edit_uint64_with_commas(count, ed1));
    }
    return 1;
 }
@@ -675,11 +896,12 @@ static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
    int count = 0;
 
    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
-      bsendmsg(ua, _("No directories unmarked.\n"));
+      ua->send_msg(_("No directories unmarked.\n"));
       return 1;
    }
 
    for (int i=1; i < ua->argc; i++) {
+      strip_trailing_slash(ua->argk[i]);
       foreach_child(node, tree->node) {
          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
@@ -691,11 +913,11 @@ static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
    }
 
    if (count == 0) {
-      bsendmsg(ua, _("No directories unmarked.\n"));
+      ua->send_msg(_("No directories unmarked.\n"));
    } else if (count == 1) {
-      bsendmsg(ua, _("1 directory unmarked.\n"));
+      ua->send_msg(_("1 directory unmarked.\n"));
    } else {
-      bsendmsg(ua, _("%d directories unmarked.\n"), count);
+      ua->send_msg(_("%d directories unmarked.\n"), count);
    }
    return 1;
 }