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