]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_tree.c
ebl Add new status storage slots command
[bacula/bacula] / bacula / src / dird / ua_tree.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2002-2008 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU General Public
10    License as published by the Free Software Foundation and included
11    in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of John Walker.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28 /*
29  *
30  *   Bacula Director -- User Agent Database File tree for Restore
31  *      command. This file interacts with the user implementing the
32  *      UA tree commands.
33  *
34  *     Kern Sibbald, July MMII
35  *
36  *   Version $Id$
37  */
38
39 #include "bacula.h"
40 #include "dird.h"
41 #ifdef HAVE_FNMATCH
42 #include <fnmatch.h>
43 #else
44 #include "lib/fnmatch.h"
45 #endif
46 #include "findlib/find.h"
47
48
49 /* Forward referenced commands */
50
51 static int markcmd(UAContext *ua, TREE_CTX *tree);
52 static int markdircmd(UAContext *ua, TREE_CTX *tree);
53 static int countcmd(UAContext *ua, TREE_CTX *tree);
54 static int findcmd(UAContext *ua, TREE_CTX *tree);
55 static int lscmd(UAContext *ua, TREE_CTX *tree);
56 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree);
57 static int dircmd(UAContext *ua, TREE_CTX *tree);
58 static int dot_dircmd(UAContext *ua, TREE_CTX *tree);
59 static int estimatecmd(UAContext *ua, TREE_CTX *tree);
60 static int helpcmd(UAContext *ua, TREE_CTX *tree);
61 static int cdcmd(UAContext *ua, TREE_CTX *tree);
62 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
63 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree);
64 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
65 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree);
66 static int quitcmd(UAContext *ua, TREE_CTX *tree);
67 static int donecmd(UAContext *ua, TREE_CTX *tree);
68
69
70 struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
71 static struct cmdstruct commands[] = {
72  { NT_("cd"),         cdcmd,        _("change current directory")},
73  { NT_("count"),      countcmd,     _("count marked files in and below the cd")},
74  { NT_("dir"),        dircmd,       _("long list current directory, wildcards allowed")},
75  { NT_(".dir"),       dot_dircmd,   _("long list current directory, wildcards allowed")},
76  { NT_("done"),       donecmd,      _("leave file selection mode")},
77  { NT_("estimate"),   estimatecmd,  _("estimate restore size")},
78  { NT_("exit"),       donecmd,      _("same as done command")},
79  { NT_("find"),       findcmd,      _("find files, wildcards allowed")},
80  { NT_("help"),       helpcmd,      _("print help")},
81  { NT_("ls"),         lscmd,        _("list current directory, wildcards allowed")},
82  { NT_("lsmark"),     lsmarkcmd,    _("list the marked files in and below the cd")},
83  { NT_("mark"),       markcmd,      _("mark dir/file to be restored recursively, wildcards allowed")},
84  { NT_("markdir"),    markdircmd,   _("mark directory name to be restored (no files)")},
85  { NT_("pwd"),        pwdcmd,       _("print current working directory")},
86  { NT_(".pwd"),       dot_pwdcmd,   _("print current working directory")},
87  { NT_("unmark"),     unmarkcmd,    _("unmark dir/file to be restored recursively in dir")},
88  { NT_("unmarkdir"),  unmarkdircmd, _("unmark directory name only no recursion")},
89  { NT_("quit"),       quitcmd,      _("quit and do not do restore")},
90  { NT_("?"),          helpcmd,      _("print help")},
91              };
92 #define comsize ((int)(sizeof(commands)/sizeof(struct cmdstruct)))
93
94 /*
95  * Enter a prompt mode where the user can select/deselect
96  *  files to be restored. This is sort of like a mini-shell
97  *  that allows "cd", "pwd", "add", "rm", ...
98  */
99 bool user_select_files_from_tree(TREE_CTX *tree)
100 {
101    char cwd[2000];
102    bool stat;
103    /* Get a new context so we don't destroy restore command args */
104    UAContext *ua = new_ua_context(tree->ua->jcr);
105    ua->UA_sock = tree->ua->UA_sock;   /* patch in UA socket */
106    ua->api = tree->ua->api;           /* keep API flag too */
107    BSOCK *user = ua->UA_sock;
108
109    ua->send_msg(_(
110       "\nYou are now entering file selection mode where you add (mark) and\n"
111       "remove (unmark) files to be restored. No files are initially added, unless\n"
112       "you used the \"all\" keyword on the command line.\n"
113       "Enter \"done\" to leave this mode.\n\n"));
114    if (ua->api) user->signal(BNET_START_RTREE);
115    /*
116     * Enter interactive command handler allowing selection
117     *  of individual files.
118     */
119    tree->node = (TREE_NODE *)tree->root;
120    tree_getpath(tree->node, cwd, sizeof(cwd));
121    ua->send_msg(_("cwd is: %s\n"), cwd);
122    for ( ;; ) {
123       int found, len, i;
124       if (!get_cmd(ua, "$ ")) {
125          break;
126       }
127       if (ua->api) user->signal(BNET_CMD_BEGIN);
128       parse_args_only(ua->cmd, &ua->args, &ua->argc, ua->argk, ua->argv, MAX_CMD_ARGS);
129       if (ua->argc == 0) {
130          ua->warning_msg(_("Invalid command. Enter \"done\" to exit.\n"));
131          if (ua->api) user->signal(BNET_CMD_FAILED);
132          continue;
133       }
134
135       len = strlen(ua->argk[0]);
136       found = 0;
137       stat = false;
138       for (i=0; i<comsize; i++)       /* search for command */
139          if (strncasecmp(ua->argk[0],  _(commands[i].key), len) == 0) {
140             stat = (*commands[i].func)(ua, tree);   /* go execute command */
141             found = 1;
142             break;
143          }
144       if (!found) {
145          ua->warning_msg(_("Invalid command. Enter \"done\" to exit.\n"));
146          if (ua->api) user->signal(BNET_CMD_FAILED);
147          continue;
148       }
149       if (ua->api) user->signal(BNET_CMD_OK);
150       if (!stat) {
151          break;
152       }
153    }
154    if (ua->api) user->signal(BNET_END_RTREE);
155    ua->UA_sock = NULL;                /* don't release restore socket */
156    stat = !ua->quit;
157    ua->quit = false;
158    free_ua_context(ua);               /* get rid of temp UA context */
159    return stat;
160 }
161
162
163 /*
164  * This callback routine is responsible for inserting the
165  *  items it gets into the directory tree. For each JobId selected
166  *  this routine is called once for each file. We do not allow
167  *  duplicate filenames, but instead keep the info from the most
168  *  recent file entered (i.e. the JobIds are assumed to be sorted)
169  *
170  *   See uar_sel_files in sql_cmds.c for query that calls us.
171  *      row[0]=Path, row[1]=Filename, row[2]=FileIndex
172  *      row[3]=JobId row[4]=LStat
173  */
174 int insert_tree_handler(void *ctx, int num_fields, char **row)
175 {
176    struct stat statp;
177    TREE_CTX *tree = (TREE_CTX *)ctx;
178    TREE_NODE *node;
179    int type;
180    bool hard_link, ok;
181    int FileIndex;
182    JobId_t JobId;
183
184 // Dmsg4(000, "Path=%s%s FI=%s JobId=%s\n", row[0], row[1],
185 //    row[2], row[3]);
186    if (*row[1] == 0) {                 /* no filename => directory */
187       if (!IsPathSeparator(*row[0])) { /* Must be Win32 directory */
188          type = TN_DIR_NLS;
189       } else {
190          type = TN_DIR;
191       }
192    } else {
193       type = TN_FILE;
194    }
195    hard_link = (decode_LinkFI(row[4], &statp) != 0);
196    node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
197    JobId = str_to_int64(row[3]);
198    FileIndex = str_to_int64(row[2]);
199    /*
200     * - The first time we see a file (node->inserted==true), we accept it.
201     * - In the same JobId, we accept only the first copy of a
202     *   hard linked file (the others are simply pointers).
203     * - In the same JobId, we accept the last copy of any other
204     *   file -- in particular directories.
205     *
206     * All the code to set ok could be condensed to a single
207     *  line, but it would be even harder to read.
208     */
209    ok = true;
210    if (!node->inserted && JobId == node->JobId) {
211       if ((hard_link && FileIndex > node->FileIndex) ||
212           (!hard_link && FileIndex < node->FileIndex)) {
213          ok = false;
214       }
215    }
216    if (ok) {
217       node->hard_link = hard_link;
218       node->FileIndex = FileIndex;
219       node->JobId = JobId;
220       node->type = type;
221       node->soft_link = S_ISLNK(statp.st_mode) != 0;
222       if (tree->all) {
223          node->extract = true;          /* extract all by default */
224          if (type == TN_DIR || type == TN_DIR_NLS) {
225             node->extract_dir = true;   /* if dir, extract it */
226          }
227       }
228    }
229    if (node->inserted) {
230       tree->FileCount++;
231       if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
232          tree->ua->send_msg("+");
233          tree->LastCount = tree->FileCount;
234       }
235    }
236    tree->cnt++;
237    return 0;
238 }
239
240
241 /*
242  * Set extract to value passed. We recursively walk
243  *  down the tree setting all children if the
244  *  node is a directory.
245  */
246 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
247 {
248    TREE_NODE *n;
249    FILE_DBR fdbr;
250    struct stat statp;
251    int count = 0;
252
253    node->extract = extract;
254    if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
255       node->extract_dir = extract;    /* set/clear dir too */
256    }
257    if (node->type != TN_NEWDIR) {
258       count++;
259    }
260    /* For a non-file (i.e. directory), we see all the children */
261    if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
262       /* Recursive set children within directory */
263       foreach_child(n, node) {
264          count += set_extract(ua, n, tree, extract);
265       }
266       /*
267        * Walk up tree marking any unextracted parent to be
268        * extracted.
269        */
270       if (extract) {
271          while (node->parent && !node->parent->extract_dir) {
272             node = node->parent;
273             node->extract_dir = true;
274          }
275       }
276    } else if (extract) {
277       char cwd[2000];
278       /*
279        * Ordinary file, we get the full path, look up the
280        * attributes, decode them, and if we are hard linked to
281        * a file that was saved, we must load that file too.
282        */
283       tree_getpath(node, cwd, sizeof(cwd));
284       fdbr.FileId = 0;
285       fdbr.JobId = node->JobId;
286       if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
287          int32_t LinkFI;
288          decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
289          /*
290           * If we point to a hard linked file, traverse the tree to
291           * find that file, and mark it to be restored as well. It
292           * must have the Link we just obtained and the same JobId.
293           */
294          if (LinkFI) {
295             for (n=first_tree_node(tree->root); n; n=next_tree_node(n)) {
296                if (n->FileIndex == LinkFI && n->JobId == node->JobId) {
297                   n->extract = true;
298                   if (n->type == TN_DIR || n->type == TN_DIR_NLS) {
299                      n->extract_dir = true;
300                   }
301                   break;
302                }
303             }
304          }
305       }
306    }
307    return count;
308 }
309
310 static void strip_trailing_slash(char *arg)
311 {
312    int len = strlen(arg);
313    if (len == 0) {
314       return;
315    }
316    len--;
317    if (arg[len] == '/') {       /* strip any trailing slash */
318       arg[len] = 0;
319    }
320 }
321
322 /*
323  * Recursively mark the current directory to be restored as
324  *  well as all directories and files below it.
325  */
326 static int markcmd(UAContext *ua, TREE_CTX *tree)
327 {
328    TREE_NODE *node;
329    int count = 0;
330    char ec1[50];
331
332    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
333       ua->send_msg(_("No files marked.\n"));
334       return 1;
335    }
336    for (int i=1; i < ua->argc; i++) {
337       strip_trailing_slash(ua->argk[i]);
338       foreach_child(node, tree->node) {
339          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
340             count += set_extract(ua, node, tree, true);
341          }
342       }
343    }
344    if (count == 0) {
345       ua->send_msg(_("No files marked.\n"));
346    } else if (count == 1) {
347       ua->send_msg(_("1 file marked.\n"));
348    } else {
349       ua->send_msg(_("%s files marked.\n"),
350                edit_uint64_with_commas(count, ec1));
351    }
352    return 1;
353 }
354
355 static int markdircmd(UAContext *ua, TREE_CTX *tree)
356 {
357    TREE_NODE *node;
358    int count = 0;
359    char ec1[50];
360
361    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
362       ua->send_msg(_("No files marked.\n"));
363       return 1;
364    }
365    for (int i=1; i < ua->argc; i++) {
366       strip_trailing_slash(ua->argk[i]);
367       foreach_child(node, tree->node) {
368          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
369             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
370                node->extract_dir = true;
371                count++;
372             }
373          }
374       }
375    }
376    if (count == 0) {
377       ua->send_msg(_("No directories marked.\n"));
378    } else if (count == 1) {
379       ua->send_msg(_("1 directory marked.\n"));
380    } else {
381       ua->send_msg(_("%s directories marked.\n"),
382                edit_uint64_with_commas(count, ec1));
383    }
384    return 1;
385 }
386
387
388 static int countcmd(UAContext *ua, TREE_CTX *tree)
389 {
390    int total, num_extract;
391    char ec1[50], ec2[50];
392
393    total = num_extract = 0;
394    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
395       if (node->type != TN_NEWDIR) {
396          total++;
397          if (node->extract || node->extract_dir) {
398             num_extract++;
399          }
400       }
401    }
402    ua->send_msg(_("%s total files/dirs. %s marked to be restored.\n"),
403             edit_uint64_with_commas(total, ec1),
404             edit_uint64_with_commas(num_extract, ec2));
405    return 1;
406 }
407
408 static int findcmd(UAContext *ua, TREE_CTX *tree)
409 {
410    char cwd[2000];
411
412    if (ua->argc == 1) {
413       ua->send_msg(_("No file specification given.\n"));
414       return 1;      /* make it non-fatal */
415    }
416
417    for (int i=1; i < ua->argc; i++) {
418       for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
419          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
420             const char *tag;
421             tree_getpath(node, cwd, sizeof(cwd));
422             if (node->extract) {
423                tag = "*";
424             } else if (node->extract_dir) {
425                tag = "+";
426             } else {
427                tag = "";
428             }
429             ua->send_msg("%s%s\n", tag, cwd);
430          }
431       }
432    }
433    return 1;
434 }
435
436
437
438 static int lscmd(UAContext *ua, TREE_CTX *tree)
439 {
440    TREE_NODE *node;
441
442    if (!tree_node_has_child(tree->node)) {
443       return 1;
444    }
445    foreach_child(node, tree->node) {
446       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
447          const char *tag;
448          if (node->extract) {
449             tag = "*";
450          } else if (node->extract_dir) {
451             tag = "+";
452          } else {
453             tag = "";
454          }
455          ua->send_msg("%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
456       }
457    }
458    return 1;
459 }
460
461 /*
462  * Ls command that lists only the marked files
463  */
464 static void rlsmark(UAContext *ua, TREE_NODE *tnode)
465 {
466    TREE_NODE *node;
467    if (!tree_node_has_child(tnode)) {
468       return;
469    }
470    foreach_child(node, tnode) {
471       if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
472           (node->extract || node->extract_dir)) {
473          const char *tag;
474          if (node->extract) {
475             tag = "*";
476          } else if (node->extract_dir) {
477             tag = "+";
478          } else {
479             tag = "";
480          }
481          ua->send_msg("%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
482          if (tree_node_has_child(node)) {
483             rlsmark(ua, node);
484          }
485       }
486    }
487 }
488
489 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
490 {
491    rlsmark(ua, tree->node);
492    return 1;
493 }
494
495 /*
496  * This is actually the long form used for "dir"
497  */
498 static void ls_output(guid_list *guid, char *buf, const char *fname, const char *tag, 
499                       struct stat *statp, bool dot_cmd) 
500 {
501    char *p;
502    const char *f;
503    char ec1[30];
504    char en1[30], en2[30];
505    int n;
506    time_t time;
507
508    p = encode_mode(statp->st_mode, buf);
509    if (dot_cmd) {
510       *p++ = ',';
511       n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
512       p += n;
513       n = sprintf(p, "%s,%s,", 
514                   guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
515                   guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
516       p += n;
517       n = sprintf(p, "%s,", edit_int64(statp->st_size, ec1));
518       p += n;
519       p = encode_time(statp->st_mtime, p);
520       *p++ = ',';
521       *p++ = *tag;
522       *p++ = ',';
523    } else {
524       n = sprintf(p, "  %2d ", (uint32_t)statp->st_nlink);
525       p += n;
526       n = sprintf(p, "%-8.8s %-8.8s", 
527                   guid->uid_to_name(statp->st_uid, en1, sizeof(en1)),
528                   guid->gid_to_name(statp->st_gid, en2, sizeof(en2)));
529       p += n;
530       n = sprintf(p, "%10.10s  ", edit_int64(statp->st_size, ec1));
531       p += n;
532       if (statp->st_ctime > statp->st_mtime) {
533          time = statp->st_ctime;
534       } else {
535          time = statp->st_mtime;
536       }
537       /* Display most recent time */
538       p = encode_time(time, p);
539       *p++ = ' ';
540       *p++ = *tag;
541    }
542    for (f=fname; *f; ) {
543       *p++ = *f++;
544    }
545    *p = 0;
546 }
547
548 /*
549  * Like ls command, but give more detail on each file
550  */
551 static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
552 {
553    TREE_NODE *node;
554    FILE_DBR fdbr;
555    struct stat statp;
556    char buf[1100];
557    char cwd[1100], *pcwd;
558    guid_list *guid;
559
560    if (!tree_node_has_child(tree->node)) {
561       ua->send_msg(_("Node %s has no children.\n"), tree->node->fname);
562       return 1;
563    }
564
565    guid = new_guid_list();
566    foreach_child(node, tree->node) {
567       const char *tag;
568       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
569          if (node->extract) {
570             tag = "*";
571          } else if (node->extract_dir) {
572             tag = "+";
573          } else {
574             tag = " ";
575          }
576          tree_getpath(node, cwd, sizeof(cwd));
577          fdbr.FileId = 0;
578          fdbr.JobId = node->JobId;
579          /*
580           * Strip / from soft links to directories.
581           *   This is because soft links to files have a trailing slash
582           *   when returned from tree_getpath, but db_get_file_attr...
583           *   treats soft links as files, so they do not have a trailing
584           *   slash like directory names.
585           */
586          if (node->type == TN_FILE && tree_node_has_child(node)) {
587             bstrncpy(buf, cwd, sizeof(buf));
588             pcwd = buf;
589             int len = strlen(buf);
590             if (len > 1) {
591                buf[len-1] = 0;        /* strip trailing / */
592             }
593          } else {
594             pcwd = cwd;
595          }
596          if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
597             int32_t LinkFI;
598             decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
599          } else {
600             /* Something went wrong getting attributes -- print name */
601             memset(&statp, 0, sizeof(statp));
602          }
603          ls_output(guid, buf, cwd, tag, &statp, dot_cmd);
604          ua->send_msg("%s\n", buf);
605       }
606    }
607    free_guid_list(guid);
608    return 1;
609 }
610
611 int dot_dircmd(UAContext *ua, TREE_CTX *tree)
612 {
613    return do_dircmd(ua, tree, true/*dot command*/);
614 }
615
616 static int dircmd(UAContext *ua, TREE_CTX *tree)
617 {
618    return do_dircmd(ua, tree, false/*not dot command*/);
619 }
620
621
622 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
623 {
624    int total, num_extract;
625    uint64_t total_bytes = 0;
626    FILE_DBR fdbr;
627    struct stat statp;
628    char cwd[1100];
629    char ec1[50];
630
631    total = num_extract = 0;
632    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
633       if (node->type != TN_NEWDIR) {
634          total++;
635          /* If regular file, get size */
636          if (node->extract && node->type == TN_FILE) {
637             num_extract++;
638             tree_getpath(node, cwd, sizeof(cwd));
639             fdbr.FileId = 0;
640             fdbr.JobId = node->JobId;
641             if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
642                int32_t LinkFI;
643                decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
644                if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
645                   total_bytes += statp.st_size;
646                }
647             }
648          /* Directory, count only */
649          } else if (node->extract || node->extract_dir) {
650             num_extract++;
651          }
652       }
653    }
654    ua->send_msg(_("%d total files; %d marked to be restored; %s bytes.\n"),
655             total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
656    return 1;
657 }
658
659
660
661 static int helpcmd(UAContext *ua, TREE_CTX *tree)
662 {
663    unsigned int i;
664
665    ua->send_msg(_("  Command    Description\n  =======    ===========\n"));
666    for (i=0; i<comsize; i++) {
667       /* List only non-dot commands */
668       if (commands[i].key[0] != '.') {
669          ua->send_msg("  %-10s %s\n", _(commands[i].key), _(commands[i].help));
670       }
671    }
672    ua->send_msg("\n");
673    return 1;
674 }
675
676 /*
677  * Change directories.  Note, if the user specifies x: and it fails,
678  *   we assume it is a Win32 absolute cd rather than relative and
679  *   try a second time with /x: ...  Win32 kludge.
680  */
681 static int cdcmd(UAContext *ua, TREE_CTX *tree)
682 {
683    TREE_NODE *node;
684    char cwd[2000];
685
686
687    if (ua->argc != 2) {
688       ua->error_msg(_("Too few or too many arguments. Try using double quotes.\n"));
689       return 1;
690    }
691    node = tree_cwd(ua->argk[1], tree->root, tree->node);
692    if (!node) {
693       /* Try once more if Win32 drive -- make absolute */
694       if (ua->argk[1][1] == ':') {  /* win32 drive */
695          bstrncpy(cwd, "/", sizeof(cwd));
696          bstrncat(cwd, ua->argk[1], sizeof(cwd));
697          node = tree_cwd(cwd, tree->root, tree->node);
698       }
699       if (!node) {
700          ua->warning_msg(_("Invalid path given.\n"));
701       } else {
702          tree->node = node;
703       }
704    } else {
705       tree->node = node;
706    }
707    return pwdcmd(ua, tree);
708 }
709
710 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
711 {
712    char cwd[2000];
713    tree_getpath(tree->node, cwd, sizeof(cwd));
714    if (ua->api) {
715       ua->send_msg("%s", cwd);
716    } else {
717       ua->send_msg(_("cwd is: %s\n"), cwd);
718    }
719    return 1;
720 }
721
722 static int dot_pwdcmd(UAContext *ua, TREE_CTX *tree)
723 {
724    char cwd[2000];
725    tree_getpath(tree->node, cwd, sizeof(cwd));
726    ua->send_msg("%s", cwd);
727    return 1;
728 }
729
730 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
731 {
732    TREE_NODE *node;
733    int count = 0;
734
735    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
736       ua->send_msg(_("No files unmarked.\n"));
737       return 1;
738    }
739    for (int i=1; i < ua->argc; i++) {
740       strip_trailing_slash(ua->argk[i]);
741       foreach_child(node, tree->node) {
742          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
743             count += set_extract(ua, node, tree, false);
744          }
745       }
746    }
747    if (count == 0) {
748       ua->send_msg(_("No files unmarked.\n"));
749    } else if (count == 1) {
750       ua->send_msg(_("1 file unmarked.\n"));
751    } else {
752       char ed1[50];
753       ua->send_msg(_("%s files unmarked.\n"), edit_uint64_with_commas(count, ed1));
754    }
755    return 1;
756 }
757
758 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
759 {
760    TREE_NODE *node;
761    int count = 0;
762
763    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
764       ua->send_msg(_("No directories unmarked.\n"));
765       return 1;
766    }
767
768    for (int i=1; i < ua->argc; i++) {
769       strip_trailing_slash(ua->argk[i]);
770       foreach_child(node, tree->node) {
771          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
772             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
773                node->extract_dir = false;
774                count++;
775             }
776          }
777       }
778    }
779
780    if (count == 0) {
781       ua->send_msg(_("No directories unmarked.\n"));
782    } else if (count == 1) {
783       ua->send_msg(_("1 directory unmarked.\n"));
784    } else {
785       ua->send_msg(_("%d directories unmarked.\n"), count);
786    }
787    return 1;
788 }
789
790
791 static int donecmd(UAContext *ua, TREE_CTX *tree)
792 {
793    return 0;
794 }
795
796 static int quitcmd(UAContext *ua, TREE_CTX *tree)
797 {
798    ua->quit = true;
799    return 0;
800 }