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