]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_tree.c
This commit was manufactured by cvs2svn to create tag
[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  { NT_("cd"),         cdcmd,        _("change current directory")},
59  { NT_("count"),      countcmd,     _("count marked files in and below the cd")},
60  { NT_("dir"),        dircmd,       _("long list current directory, wildcards allowed")},
61  { NT_(".dir"),       dot_dircmd,   _("long list current directory, wildcards allowed")},
62  { NT_("done"),       donecmd,      _("leave file selection mode")},
63  { NT_("estimate"),   estimatecmd,  _("estimate restore size")},
64  { NT_("exit"),       donecmd,      _("same as done command")},
65  { NT_("find"),       findcmd,      _("find files, wildcards allowed")},
66  { NT_("help"),       helpcmd,      _("print help")},
67  { NT_("ls"),         lscmd,        _("list current directory, wildcards allowed")},
68  { NT_("lsmark"),     lsmarkcmd,    _("list the marked files in and below the cd")},
69  { NT_("mark"),       markcmd,      _("mark dir/file to be restored recursively, wildcards allowed")},
70  { NT_("markdir"),    markdircmd,   _("mark directory name to be restored (no files)")},
71  { NT_("pwd"),        pwdcmd,       _("print current working directory")},
72  { NT_("unmark"),     unmarkcmd,    _("unmark dir/file to be restored recursively in dir")},
73  { NT_("unmarkdir"),  unmarkdircmd, _("unmark directory name only no recursion")},
74  { NT_("quit"),       quitcmd,      _("quit and do not do restore")},
75  { NT_("?"),          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    time_t time;
476
477    p = encode_mode(statp->st_mode, buf);
478    if (dot_cmd) {
479       *p++ = ',';
480       n = sprintf(p, "%d,", (uint32_t)statp->st_nlink);
481       p += n;
482       n = sprintf(p, "%s,%s,", getuser(statp->st_uid, en1, sizeof(en1)),
483                   getgroup(statp->st_gid, en2, sizeof(en2)));
484       p += n;
485       n = sprintf(p, "%s,", edit_uint64(statp->st_size, ec1));
486       p += n;
487       p = encode_time(statp->st_mtime, p);
488       *p++ = ',';
489       *p++ = *tag;
490       *p++ = ',';
491    } else {
492       n = sprintf(p, "  %2d ", (uint32_t)statp->st_nlink);
493       p += n;
494       n = sprintf(p, "%-8.8s %-8.8s", getuser(statp->st_uid, en1, sizeof(en1)),
495                   getgroup(statp->st_gid, en2, sizeof(en2)));
496       p += n;
497       n = sprintf(p, "%10.10s  ", edit_uint64(statp->st_size, ec1));
498       p += n;
499       time = statp->st_mtime;
500       /* Display most recent time */
501       p = encode_time(time, p);
502       *p++ = ' ';
503       *p++ = *tag;
504    }
505    for (f=fname; *f; ) {
506       *p++ = *f++;
507    }
508    *p = 0;
509 }
510
511 /*
512  * Like ls command, but give more detail on each file
513  */
514 static int do_dircmd(UAContext *ua, TREE_CTX *tree, bool dot_cmd)
515 {
516    TREE_NODE *node;
517    FILE_DBR fdbr;
518    struct stat statp;
519    char buf[1100];
520    char cwd[1100], *pcwd;
521
522    if (!tree_node_has_child(tree->node)) {
523       bsendmsg(ua, _("Node %s has no children.\n"), tree->node->fname);
524       return 1;
525    }
526
527    foreach_child(node, tree->node) {
528       const char *tag;
529       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
530          if (node->extract) {
531             tag = "*";
532          } else if (node->extract_dir) {
533             tag = "+";
534          } else {
535             tag = " ";
536          }
537          tree_getpath(node, cwd, sizeof(cwd));
538          fdbr.FileId = 0;
539          fdbr.JobId = node->JobId;
540          /*
541           * Strip / from soft links to directories.
542           *   This is because soft links to files have a trailing slash
543           *   when returned from tree_getpath, but db_get_file_attr...
544           *   treats soft links as files, so they do not have a trailing
545           *   slash like directory names.
546           */
547          if (node->type == TN_FILE && tree_node_has_child(node)) {
548             bstrncpy(buf, cwd, sizeof(buf));
549             pcwd = buf;
550             int len = strlen(buf);
551             if (len > 1) {
552                buf[len-1] = 0;        /* strip trailing / */
553             }
554          } else {
555             pcwd = cwd;
556          }
557          if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
558             int32_t LinkFI;
559             decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
560          } else {
561             /* Something went wrong getting attributes -- print name */
562             memset(&statp, 0, sizeof(statp));
563          }
564          ls_output(buf, cwd, tag, &statp, dot_cmd);
565          bsendmsg(ua, "%s\n", buf);
566       }
567    }
568    return 1;
569 }
570
571 int dot_dircmd(UAContext *ua, TREE_CTX *tree)
572 {
573    return do_dircmd(ua, tree, true/*dot command*/);
574 }
575
576 static int dircmd(UAContext *ua, TREE_CTX *tree)
577 {
578    return do_dircmd(ua, tree, false/*not dot command*/);
579 }
580
581
582 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
583 {
584    int total, num_extract;
585    uint64_t total_bytes = 0;
586    FILE_DBR fdbr;
587    struct stat statp;
588    char cwd[1100];
589    char ec1[50];
590
591    total = num_extract = 0;
592    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
593       if (node->type != TN_NEWDIR) {
594          total++;
595          /* If regular file, get size */
596          if (node->extract && node->type == TN_FILE) {
597             num_extract++;
598             tree_getpath(node, cwd, sizeof(cwd));
599             fdbr.FileId = 0;
600             fdbr.JobId = node->JobId;
601             if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
602                int32_t LinkFI;
603                decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
604                if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
605                   total_bytes += statp.st_size;
606                }
607             }
608          /* Directory, count only */
609          } else if (node->extract || node->extract_dir) {
610             num_extract++;
611          }
612       }
613    }
614    bsendmsg(ua, _("%d total files; %d marked to be restored; %s bytes.\n"),
615             total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
616    return 1;
617 }
618
619
620
621 static int helpcmd(UAContext *ua, TREE_CTX *tree)
622 {
623    unsigned int i;
624
625    bsendmsg(ua, _("  Command    Description\n  =======    ===========\n"));
626    for (i=0; i<comsize; i++) {
627       /* List only non-dot commands */
628       if (commands[i].key[0] != '.') {
629          bsendmsg(ua, "  %-10s %s\n", _(commands[i].key), _(commands[i].help));
630       }
631    }
632    bsendmsg(ua, "\n");
633    return 1;
634 }
635
636 /*
637  * Change directories.  Note, if the user specifies x: and it fails,
638  *   we assume it is a Win32 absolute cd rather than relative and
639  *   try a second time with /x: ...  Win32 kludge.
640  */
641 static int cdcmd(UAContext *ua, TREE_CTX *tree)
642 {
643    TREE_NODE *node;
644    char cwd[2000];
645
646    if (ua->argc != 2) {
647       return 1;
648    }
649    strip_leading_space(ua->argk[1]);
650    node = tree_cwd(ua->argk[1], tree->root, tree->node);
651    if (!node) {
652       /* Try once more if Win32 drive -- make absolute */
653       if (ua->argk[1][1] == ':') {  /* win32 drive */
654          bstrncpy(cwd, "/", sizeof(cwd));
655          bstrncat(cwd, ua->argk[1], sizeof(cwd));
656          node = tree_cwd(cwd, tree->root, tree->node);
657       }
658       if (!node) {
659          bsendmsg(ua, _("Invalid path given.\n"));
660       } else {
661          tree->node = node;
662       }
663    } else {
664       tree->node = node;
665    }
666    tree_getpath(tree->node, cwd, sizeof(cwd));
667    bsendmsg(ua, _("cwd is: %s\n"), cwd);
668    return 1;
669 }
670
671 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
672 {
673    char cwd[2000];
674    tree_getpath(tree->node, cwd, sizeof(cwd));
675    bsendmsg(ua, _("cwd is: %s\n"), cwd);
676    return 1;
677 }
678
679
680 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
681 {
682    TREE_NODE *node;
683    int count = 0;
684
685    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
686       bsendmsg(ua, _("No files unmarked.\n"));
687       return 1;
688    }
689    for (int i=1; i < ua->argc; i++) {
690       foreach_child(node, tree->node) {
691          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
692             count += set_extract(ua, node, tree, false);
693          }
694       }
695    }
696    if (count == 0) {
697       bsendmsg(ua, _("No files unmarked.\n"));
698    } else if (count == 1) {
699       bsendmsg(ua, _("1 file unmarked.\n"));
700    } else {
701       bsendmsg(ua, _("%d files unmarked.\n"), count);
702    }
703    return 1;
704 }
705
706 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
707 {
708    TREE_NODE *node;
709    int count = 0;
710
711    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
712       bsendmsg(ua, _("No directories unmarked.\n"));
713       return 1;
714    }
715
716    for (int i=1; i < ua->argc; i++) {
717       foreach_child(node, tree->node) {
718          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
719             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
720                node->extract_dir = false;
721                count++;
722             }
723          }
724       }
725    }
726
727    if (count == 0) {
728       bsendmsg(ua, _("No directories unmarked.\n"));
729    } else if (count == 1) {
730       bsendmsg(ua, _("1 directory unmarked.\n"));
731    } else {
732       bsendmsg(ua, _("%d directories unmarked.\n"), count);
733    }
734    return 1;
735 }
736
737
738 static int donecmd(UAContext *ua, TREE_CTX *tree)
739 {
740    return 0;
741 }
742
743 static int quitcmd(UAContext *ua, TREE_CTX *tree)
744 {
745    ua->quit = true;
746    return 0;
747 }