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