]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_restore.c
restore cmd + misc -- see kes04Aug02
[bacula/bacula] / bacula / src / dird / ua_restore.c
1 /*
2  *
3  *   Bacula Director -- User Agent Database restore Command
4  *      Creates a bootstrap file for restoring files
5  *
6  *     Kern Sibbald, July MMII
7  *
8  *   Version $Id$
9  */
10
11 /*
12    Copyright (C) 2002 Kern Sibbald and John Walker
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 as
16    published by the Free Software Foundation; either version 2 of
17    the License, or (at your option) any later version.
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 GNU
22    General Public License for more details.
23
24    You should have received a copy of the GNU General Public
25    License along with this program; if not, write to the Free
26    Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
27    MA 02111-1307, USA.
28
29  */
30
31 #include "bacula.h"
32 #include "dird.h"
33 #include "ua.h"
34 #include <fnmatch.h>
35
36
37
38 /* Imported functions */
39 extern char *uar_list_jobs;
40 extern char *uar_file;
41 extern char *uar_sel_files;
42 extern char *uar_del_temp;
43 extern char *uar_del_temp1;
44 extern char *uar_create_temp;
45 extern char *uar_create_temp1;
46 extern char *uar_last_full;
47 extern char *uar_full;
48 extern char *uar_inc;
49 extern char *uar_list_temp;
50 extern char *uar_sel_jobid_temp;
51
52 /* Context for insert_tree_handler() */
53 typedef struct s_tree_ctx {
54    TREE_ROOT *root;                   /* root */
55    TREE_NODE *node;                   /* current node */
56    TREE_NODE *avail_node;             /* unused node last insert */
57    int cnt;                           /* count for user feedback */
58    UAContext *ua;
59 } TREE_CTX;
60
61 struct s_full_ctx {
62    btime_t JobTDate;
63    uint32_t ClientId;
64    uint32_t TotalFiles;
65    char JobIds[200];
66 };
67
68
69 /* FileIndex entry in bootstrap record */
70 typedef struct s_rbsr_findex {
71    struct s_rbsr_findex *next;
72    int32_t findex;
73    int32_t findex2;
74 } RBSR_FINDEX;
75
76 /* Restore bootstrap record -- not the real one, but useful here */
77 typedef struct s_rbsr {
78    struct s_rbsr *next;               /* next JobId */
79    uint32_t JobId;                    /* JobId this bsr */
80    uint32_t VolSessionId;                   
81    uint32_t VolSessionTime;
82    char *VolumeName;                  /* Volume name */
83    RBSR_FINDEX *fi;                   /* File indexes this JobId */
84 } RBSR;
85
86 /* Forward referenced functions */
87 static RBSR *new_bsr();
88 static void free_bsr(RBSR *bsr);
89 static void print_bsr(UAContext *ua, RBSR *bsr);
90 static int  complete_bsr(UAContext *ua, RBSR *bsr);
91 static int insert_tree_handler(void *ctx, int num_fields, char **row);
92 static void add_findex(RBSR *bsr, uint32_t JobId, int32_t findex);
93 static int last_full_handler(void *ctx, int num_fields, char **row);
94 static int jobid_handler(void *ctx, int num_fields, char **row);
95 static int next_jobid_from_list(char **p, uint32_t *JobId);
96 static int user_select_jobids(UAContext *ua, struct s_full_ctx *full);
97 static void user_select_files(TREE_CTX *tree);
98
99
100 /*
101  *   Restore files
102  *
103  */
104 int restorecmd(UAContext *ua, char *cmd)
105 {
106    POOLMEM *query;
107    TREE_CTX tree;
108    JobId_t JobId;
109    char *p;
110    RBSR *bsr;
111    char *nofname = "";
112    struct s_full_ctx full;
113
114    if (!open_db(ua)) {
115       return 0;
116    }
117
118    memset(&tree, 0, sizeof(TREE_CTX));
119    memset(&full, 0, sizeof(full));
120
121    if (!user_select_jobids(ua, &full)) {
122       return 0;
123    }
124
125
126    /* 
127     * Build the directory tree  
128     */
129    tree.root = new_tree(full.TotalFiles);
130    tree.root->fname = nofname;
131    tree.ua = ua;
132    query = get_pool_memory(PM_MESSAGE);
133    for (p=full.JobIds; next_jobid_from_list(&p, &JobId) > 0; ) {
134       bsendmsg(ua, _("Building directory tree for JobId %u ...\n"), JobId);
135       Mmsg(&query, uar_sel_files, JobId);
136       if (!db_sql_query(ua->db, query, insert_tree_handler, (void *)&tree)) {
137          bsendmsg(ua, "%s", db_strerror(ua->db));
138       }
139    }
140    bsendmsg(ua, "\n");
141    free_pool_memory(query);
142
143    /* Let the user select which files to restore */
144    user_select_files(&tree);
145
146    /*
147     * Walk down through the tree finding all files marked to be 
148     *  extracted making a bootstrap file.
149     */
150    bsr = new_bsr();
151    for (TREE_NODE *node=first_tree_node(tree.root); node; node=next_tree_node(node)) {
152       Dmsg2(400, "FI=%d node=0x%x\n", node->FileIndex, node);
153       if (node->extract) {
154          Dmsg2(400, "type=%d FI=%d\n", node->type, node->FileIndex);
155          add_findex(bsr, node->JobId, node->FileIndex);
156       }
157    }
158
159    free_tree(tree.root);              /* free the directory tree */
160
161    if (bsr->JobId) {
162       complete_bsr(ua, bsr);          /* find Vol, SessId, SessTime from JobIds */
163       print_bsr(ua, bsr);
164    } else {
165       bsendmsg(ua, _("No files selected to restore.\n"));
166    }
167    free_bsr(bsr);
168
169    bsendmsg(ua, _("Restore command done.\n"));
170    return 1;
171 }
172
173 /*
174  * The first step in the restore process is for the user to 
175  *  select a list of JobIds from which he will subsequently
176  *  select which files are to be restored.
177  */
178 static int user_select_jobids(UAContext *ua, struct s_full_ctx *full)
179 {
180    char *p;
181    JobId_t JobId;
182    JOB_DBR jr;
183    POOLMEM *query;
184    int done = 0;
185    char *list[] = { 
186       "List last 20 Jobs run",
187       "List Jobs where a given File is saved",
188       "Enter list of JobIds to select",
189       "Enter SQL list command", 
190       "Select the most recent backup for a client"
191       "Cancel",
192       NULL };
193
194    bsendmsg(ua, _("\nFirst you select one or more JobIds that contain files\n"
195                   "to be restored. You will be presented several methods\n"
196                   "of specifying the JobIds. Then you will be allowed to\n"
197                   "select which files from those JobIds are to be restored.\n\n"));
198
199    for ( ; !done; ) {
200       start_prompt(ua, _("To select the JobIds, you have the following choices:\n"));
201       for (int i=0; list[i]; i++) {
202          add_prompt(ua, list[i]);
203       }
204       done = 1;
205       switch (do_prompt(ua, "Select item: ", NULL)) {
206       case -1:                        /* error */
207          return 0;
208       case 0:                         /* list last 20 Jobs run */
209          db_list_sql_query(ua->db, uar_list_jobs, prtit, ua, 1);
210          done = 0;
211          break;
212       case 1:                         /* list where a file is saved */
213          if (!get_cmd(ua, _("Enter Filename: "))) {
214             return 0;
215          }
216          query = get_pool_memory(PM_MESSAGE);
217          Mmsg(&query, uar_file, ua->cmd);
218          db_list_sql_query(ua->db, query, prtit, ua, 1);
219          free_pool_memory(query);
220          done = 0;
221          break;
222       case 2:                         /* enter a list of JobIds */
223          if (!get_cmd(ua, _("Enter JobId(s), comma separated, to restore: "))) {
224             return 0;
225          }
226          bstrncpy(full->JobIds, ua->cmd, sizeof(full->JobIds));
227          break;
228       case 3:                         /* Enter an SQL list command */
229          if (!get_cmd(ua, _("Enter SQL list command: "))) {
230             return 0;
231          }
232          db_list_sql_query(ua->db, ua->cmd, prtit, ua, 1);
233          done = 0;
234          break;
235       case 4:                         /* Select the most recent backups */
236          db_sql_query(ua->db, uar_del_temp, NULL, NULL);
237          db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
238          if (!db_sql_query(ua->db, uar_create_temp, NULL, NULL)) {
239             bsendmsg(ua, "%s\n", db_strerror(ua->db));
240          }
241          if (!db_sql_query(ua->db, uar_create_temp1, NULL, NULL)) {
242             bsendmsg(ua, "%s\n", db_strerror(ua->db));
243          }
244          if (!get_cmd(ua, _("Enter Client name: "))) {
245             return 0;
246          }
247          query = get_pool_memory(PM_MESSAGE);
248          Mmsg(&query, uar_last_full, ua->cmd);
249          /* Find JobId of full Backup of system */
250          if (!db_sql_query(ua->db, query, NULL, NULL)) {
251             bsendmsg(ua, "%s\n", db_strerror(ua->db));
252          }
253          /* Find all Volumes used by that JobId */
254          if (!db_sql_query(ua->db, uar_full, NULL,NULL)) {
255             bsendmsg(ua, "%s\n", db_strerror(ua->db));
256          }
257          /* Note, this is needed as I don't seem to get the callback
258           * from the call just above.
259           */
260          if (!db_sql_query(ua->db, "SELECT * from temp1", last_full_handler, (void *)full)) {
261             bsendmsg(ua, "%s\n", db_strerror(ua->db));
262          }
263          /* Now find all Incremental Jobs */
264          Mmsg(&query, uar_inc, (uint32_t)full->JobTDate, full->ClientId);
265          if (!db_sql_query(ua->db, query, NULL, NULL)) {
266             bsendmsg(ua, "%s\n", db_strerror(ua->db));
267          }
268          free_pool_memory(query);
269          db_list_sql_query(ua->db, uar_list_temp, prtit, ua, 1);
270
271          if (!db_sql_query(ua->db, uar_sel_jobid_temp, jobid_handler, (void *)full)) {
272             bsendmsg(ua, "%s\n", db_strerror(ua->db));
273          }
274          db_sql_query(ua->db, uar_del_temp, NULL, NULL);
275          db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
276          break;
277       case 5:
278          return 0;
279       }
280    }
281
282    if (*full->JobIds == 0) {
283       bsendmsg(ua, _("No Jobs selected.\n"));
284       return 0;
285    }
286    bsendmsg(ua, _("You have selected the following JobId: %s\n"), full->JobIds);
287
288    memset(&jr, 0, sizeof(JOB_DBR));
289
290    for (p=full->JobIds; ; ) {
291       int stat = next_jobid_from_list(&p, &JobId);
292       if (stat < 0) {
293          bsendmsg(ua, _("Invalid JobId in list.\n"));
294          return 0;
295       }
296       if (stat == 0) {
297          break;
298       }
299       jr.JobId = JobId;
300       if (!db_get_job_record(ua->db, &jr)) {
301          bsendmsg(ua, _("Unable to get Job record. ERR=%s\n"), db_strerror(ua->db));
302          return 0;
303       }
304       full->TotalFiles = jr.JobFiles;
305    }
306    return 1;
307 }
308
309 static int next_jobid_from_list(char **p, uint32_t *JobId)
310 {
311    char jobid[30];
312    int i;
313    char *q = *p;
314
315    jobid[0] = 0;
316    for (i=0; i<(int)sizeof(jobid); i++) {
317       if (*q == ',') {
318          q++;
319          break;
320       }
321       jobid[i] = *q++;
322       jobid[i+1] = 0;
323    }
324    if (jobid[0] == 0 || !is_a_number(jobid)) {
325       return 0;
326    }
327    *p = q;
328    *JobId = strtoul(jobid, NULL, 10);
329    if (errno) {
330       return 0;
331    }
332    return 1;
333 }
334
335 /*
336  * Callback handler make list of JobIds
337  */
338 static int jobid_handler(void *ctx, int num_fields, char **row)
339 {
340    struct s_full_ctx *full = (struct s_full_ctx *)ctx;
341
342    if (strlen(full->JobIds)+strlen(row[0])+2 < sizeof(full->JobIds)) {
343       if (full->JobIds[0] != 0) {
344          strcat(full->JobIds, ",");
345       }
346       strcat(full->JobIds, row[0]);
347    }
348
349    return 0;
350 }
351
352
353 /*
354  * Callback handler to pickup last Full backup JobId and ClientId
355  */
356 static int last_full_handler(void *ctx, int num_fields, char **row)
357 {
358    struct s_full_ctx *full = (struct s_full_ctx *)ctx;
359
360    full->JobTDate = atoi(row[1]);
361    full->ClientId = atoi(row[2]);
362
363    return 0;
364 }
365
366
367
368
369 /* Forward referenced commands */
370
371 static int addcmd(UAContext *ua, TREE_CTX *tree);
372 static int countcmd(UAContext *ua, TREE_CTX *tree);
373 static int findcmd(UAContext *ua, TREE_CTX *tree);
374 static int lscmd(UAContext *ua, TREE_CTX *tree);
375 static int helpcmd(UAContext *ua, TREE_CTX *tree);
376 static int cdcmd(UAContext *ua, TREE_CTX *tree);
377 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
378 static int rmcmd(UAContext *ua, TREE_CTX *tree);
379 static int quitcmd(UAContext *ua, TREE_CTX *tree);
380
381
382 struct cmdstruct { char *key; int (*func)(UAContext *ua, TREE_CTX *tree); char *help; }; 
383 static struct cmdstruct commands[] = {
384  { N_("add"),        addcmd,       _("add file")},
385  { N_("count"),      countcmd,     _("count files")},
386  { N_("find"),       findcmd,      _("find files")},
387  { N_("ls"),         lscmd,        _("list current directory")},    
388  { N_("dir"),        lscmd,        _("list current directory")},    
389  { N_("help"),       helpcmd,      _("print help")},
390  { N_("cd"),         cdcmd,        _("change directory")},
391  { N_("pwd"),        pwdcmd,       _("print directory")},
392  { N_("rm"),         rmcmd,        _("remove a file")},
393  { N_("remove"),     rmcmd,        _("remove a file")},
394  { N_("done"),       quitcmd,      _("quit")},
395  { N_("exit"),       quitcmd,      _("exit = quit")},
396  { N_("?"),          helpcmd,      _("print help")},    
397              };
398 #define comsize (sizeof(commands)/sizeof(struct cmdstruct))
399
400
401 /*
402  * Enter a prompt mode where the user can select/deselect
403  *  files to be restored. This is sort of like a mini-shell
404  *  that allows "cd", "pwd", "add", "rm", ...
405  */
406 static void user_select_files(TREE_CTX *tree)
407 {
408    char cwd[2000];
409    /*
410     * Enter interactive command handler allowing selection
411     *  of individual files.
412     */
413    tree->node = (TREE_NODE *)tree->root;
414    tree_getpath(tree->node, cwd, sizeof(cwd));
415    bsendmsg(tree->ua, _("cwd is: %s\n"), cwd);
416    for ( ;; ) {       
417       int found, len, stat, i;
418       if (!get_cmd(tree->ua, "$ ")) {
419          break;
420       }
421       parse_command_args(tree->ua);
422       if (tree->ua->argc == 0) {
423          return;
424       }
425
426       len = strlen(tree->ua->argk[0]);
427       found = 0;
428       for (i=0; i<(int)comsize; i++)       /* search for command */
429          if (strncasecmp(tree->ua->argk[0],  _(commands[i].key), len) == 0) {
430             stat = (*commands[i].func)(tree->ua, tree);   /* go execute command */
431             found = 1;
432             break;
433          }
434       if (!found) {
435          bsendmsg(tree->ua, _("Illegal command. Enter \"done\" to end.\n"));
436       }
437       if (!stat) {
438          break;
439       }
440    }
441 }
442
443
444 static RBSR_FINDEX *new_findex() 
445 {
446    RBSR_FINDEX *fi = (RBSR_FINDEX *)malloc(sizeof(RBSR_FINDEX));
447    memset(fi, 0, sizeof(RBSR_FINDEX));
448    return fi;
449 }
450
451 static void free_findex(RBSR_FINDEX *fi)
452 {
453    if (fi) {
454       free_findex(fi->next);
455       free(fi);
456    }
457 }
458
459 static void print_findex(UAContext *ua, RBSR_FINDEX *fi)
460 {
461    if (fi) {
462       if (fi->findex == fi->findex2) {
463          bsendmsg(ua, "FileIndex=%d\n", fi->findex);
464       } else {
465          bsendmsg(ua, "FileIndex=%d-%d\n", fi->findex, fi->findex2);
466       }
467       print_findex(ua, fi->next);
468    }
469 }
470
471 static RBSR *new_bsr()
472 {
473    RBSR *bsr = (RBSR *)malloc(sizeof(RBSR));
474    memset(bsr, 0, sizeof(RBSR));
475    return bsr;
476 }
477
478 static void free_bsr(RBSR *bsr)
479 {
480    if (bsr) {
481       free_findex(bsr->fi);
482       free_bsr(bsr->next);
483       if (bsr->VolumeName) {
484          free(bsr->VolumeName);
485       }
486       free(bsr);
487    }
488 }
489
490 /*
491  * Complete the BSR by filling in the VolumeName and
492  *  VolSessionId and VolSessionTime
493  */
494 static int complete_bsr(UAContext *ua, RBSR *bsr)
495 {
496    JOB_DBR jr;
497    char VolumeNames[1000];            /* ****FIXME**** */
498
499    if (bsr) {
500       memset(&jr, 0, sizeof(jr));
501       jr.JobId = bsr->JobId;
502       if (!db_get_job_record(ua->db, &jr)) {
503          bsendmsg(ua, _("Unable to get Job record. ERR=%s\n"), db_strerror(ua->db));
504          return 0;
505       }
506       bsr->VolSessionId = jr.VolSessionId;
507       bsr->VolSessionTime = jr.VolSessionTime;
508       if (!db_get_job_volume_names(ua->db, bsr->JobId, VolumeNames)) {
509          bsendmsg(ua, _("Unable to get Job Volumes. ERR=%s\n"), db_strerror(ua->db));
510          return 0;
511       }
512       bsr->VolumeName = bstrdup(VolumeNames);
513       return complete_bsr(ua, bsr->next);
514    }
515    return 1;
516 }
517
518
519 static void print_bsr(UAContext *ua, RBSR *bsr)
520 {
521    if (bsr) {
522       if (bsr->VolumeName) {
523          bsendmsg(ua, "VolumeName=%s\n", bsr->VolumeName);
524       }
525 //    bsendmsg(ua, "JobId=%u\n", bsr->JobId);
526       bsendmsg(ua, "VolSessionId=%u\n", bsr->VolSessionId);
527       bsendmsg(ua, "VolSessionTime=%u\n", bsr->VolSessionTime);
528       print_findex(ua, bsr->fi);
529       print_bsr(ua, bsr->next);
530    }
531 }
532
533
534 /*
535  * Add a FileIndex to the list of BootStrap records.
536  *  Here we are only dealing with JobId's and the FileIndexes
537  *  associated with those JobIds.
538  */
539 static void add_findex(RBSR *bsr, uint32_t JobId, int32_t findex)
540 {
541    RBSR *nbsr;
542    RBSR_FINDEX *fi, *lfi;
543
544    if (findex == 0) {
545       return;                         /* probably a dummy directory */
546    }
547
548    if (!bsr->fi) {                    /* if no FI add one */
549       /* This is the first FileIndex item in the chain */
550       bsr->fi = new_findex();
551       bsr->JobId = JobId;
552       bsr->fi->findex = findex;
553       bsr->fi->findex2 = findex;
554       return;
555    }
556    /* Walk down list of bsrs until we find the JobId */
557    if (bsr->JobId != JobId) {
558       for (nbsr=bsr->next; nbsr; nbsr=nbsr->next) {
559          if (nbsr->JobId == JobId) {
560             bsr = nbsr;
561             break;
562          }
563       }
564
565       if (!nbsr) {                    /* Must add new JobId */
566          /* Add new JobId at end of chain */
567          for (nbsr=bsr; nbsr->next; nbsr=nbsr->next) 
568             {  }
569          nbsr->next = new_bsr();
570          nbsr->next->JobId = JobId;
571          nbsr->next->fi = new_findex();
572          nbsr->next->fi->findex = findex;
573          nbsr->next->fi->findex2 = findex;
574          return;
575       }
576    }
577
578    /* 
579     * At this point, bsr points to bsr containing JobId,
580     *  and we are sure that there is at least one fi record.
581     */
582    lfi = fi = bsr->fi;
583    /* Check if this findex is smaller than first item */
584    if (findex < fi->findex) {
585       if ((findex+1) == fi->findex) {
586          fi->findex = findex;         /* extend down */
587          return;
588       }
589       fi = new_findex();              /* yes, insert before first item */
590       fi->findex = findex;
591       fi->findex2 = findex;
592       fi->next = lfi;
593       bsr->fi = fi;
594       return;
595    }
596    /* Walk down fi chain and find where to insert insert new FileIndex */
597    for ( ; fi; fi=fi->next) {
598       if (findex == (fi->findex2 + 1)) {  /* extend up */
599          RBSR_FINDEX *nfi;     
600          fi->findex2 = findex;
601          if (fi->next && ((findex+1) == fi->next->findex)) { 
602             Dmsg1(400, "Coallase %d\n", findex);
603             nfi = fi->next;
604             fi->findex2 = nfi->findex2;
605             fi->next = nfi->next;
606             free(nfi);
607          }
608          return;
609       }
610       if (findex < fi->findex) {      /* add before */
611          if ((findex+1) == fi->findex) {
612             fi->findex = findex;
613             return;
614          }
615          break;
616       }
617       lfi = fi;
618    }
619    /* Add to last place found */
620    fi = new_findex();
621    fi->findex = findex;
622    fi->findex2 = findex;
623    fi->next = lfi->next;
624    lfi->next = fi;
625    return;
626 }
627
628 static int insert_tree_handler(void *ctx, int num_fields, char **row)
629 {
630    TREE_CTX *tree = (TREE_CTX *)ctx;
631    char fname[2000];
632    TREE_NODE *node, *new_node;
633    int type;
634
635    strip_trailing_junk(row[1]);
636    if (*row[1] == 0) {
637       type = TN_DIR;
638    } else {
639       type = TN_FILE;
640    }
641    sprintf(fname, "%s%s", row[0], row[1]);
642    if (tree->avail_node) {
643       node = tree->avail_node;
644    } else {
645       node = new_tree_node(tree->root, type);
646       tree->avail_node = node;
647    }
648    Dmsg2(400, "FI=%d fname=%s\n", node->FileIndex, fname);
649    new_node = insert_tree_node(fname, node, tree->root, NULL);
650    /* Note, if node already exists, save new one for next time */
651    if (new_node != node) {
652       tree->avail_node = node;
653    } else {
654       tree->avail_node = NULL;
655    }
656    new_node->FileIndex = atoi(row[2]);
657    new_node->JobId = atoi(row[3]);
658    new_node->type = type;
659 #ifdef xxxxxxx
660    if (((tree->cnt) % 10000) == 0) {
661       bsendmsg(tree->ua, "%d ", tree->cnt);
662    }
663 #endif
664    tree->cnt++;
665    return 0;
666 }
667
668
669 /*
670  * Set extract to value passed. We recursively walk
671  *  down the tree setting all children.
672  */
673 static void set_extract(TREE_NODE *node, int value)
674 {
675    TREE_NODE *n;
676
677    node->extract = value;
678    if (node->type != TN_FILE) {
679       for (n=node->child; n; n=n->sibling) {
680          set_extract(n, value);
681       }
682    }
683 }
684
685 static int addcmd(UAContext *ua, TREE_CTX *tree)
686 {
687    TREE_NODE *node;
688
689    if (ua->argc < 2)
690       return 1;
691    if (!tree->node->child) {     
692       return 1;
693    }
694    for (node = tree->node->child; node; node=node->sibling) {
695       if (fnmatch(ua->argk[1], node->fname, 0) == 0) {
696          set_extract(node, 1);
697       }
698    }
699    return 1;
700 }
701
702 static int countcmd(UAContext *ua, TREE_CTX *tree)
703 {
704    int total, extract;
705
706    total = extract = 0;
707    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
708       if (node->type != TN_NEWDIR) {
709          total++;
710          if (node->extract) {
711             extract++;
712          }
713       }
714    }
715    bsendmsg(ua, "%d total files. %d marked for extraction.\n", total, extract);
716    return 1;
717 }
718
719 static int findcmd(UAContext *ua, TREE_CTX *tree)
720 {
721    char cwd[2000];
722
723    if (ua->argc == 1) {
724       bsendmsg(ua, _("No file specification given.\n"));
725       return 0;
726    }
727    
728    for (int i=1; i < ua->argc; i++) {
729       for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
730          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
731             tree_getpath(node, cwd, sizeof(cwd));
732             bsendmsg(ua, "%s\n", cwd);
733          }
734       }
735    }
736    return 1;
737 }
738
739
740
741 static int lscmd(UAContext *ua, TREE_CTX *tree)
742 {
743    TREE_NODE *node;
744
745    if (!tree->node->child) {     
746       return 1;
747    }
748    for (node = tree->node->child; node; node=node->sibling) {
749       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
750          bsendmsg(ua, "%s%s%s\n", node->extract?"*":"", node->fname,
751             (node->type==TN_DIR||node->type==TN_NEWDIR)?"/":"");
752       }
753    }
754    return 1;
755 }
756
757 static int helpcmd(UAContext *ua, TREE_CTX *tree) 
758 {
759    unsigned int i;
760
761 /* usage(); */
762    bsendmsg(ua, _("  Command    Description\n  =======    ===========\n"));
763    for (i=0; i<comsize; i++) {
764       bsendmsg(ua, _("  %-10s %s\n"), _(commands[i].key), _(commands[i].help));
765    }
766    bsendmsg(ua, "\n");
767    return 1;
768 }
769
770 static int cdcmd(UAContext *ua, TREE_CTX *tree) 
771 {
772    TREE_NODE *node;
773    char cwd[2000];
774
775    if (ua->argc != 2) {
776       return 1;
777    }
778    node = tree_cwd(ua->argk[1], tree->root, tree->node);
779    if (!node) {
780       bsendmsg(ua, _("Invalid path given.\n"));
781    } else {
782       tree->node = node;
783    }
784    tree_getpath(tree->node, cwd, sizeof(cwd));
785    bsendmsg(ua, _("cwd is: %s\n"), cwd);
786    return 1;
787 }
788
789 static int pwdcmd(UAContext *ua, TREE_CTX *tree) 
790 {
791    char cwd[2000];
792    tree_getpath(tree->node, cwd, sizeof(cwd));
793    bsendmsg(ua, _("cwd is: %s\n"), cwd);
794    return 1;
795 }
796
797
798 static int rmcmd(UAContext *ua, TREE_CTX *tree)
799 {
800    TREE_NODE *node;
801
802    if (ua->argc < 2)
803       return 1;
804    if (!tree->node->child) {     
805       return 1;
806    }
807    for (node = tree->node->child; node; node=node->sibling) {
808       if (fnmatch(ua->argk[1], node->fname, 0) == 0) {
809          set_extract(node, 0);
810       }
811    }
812    return 1;
813 }
814
815 static int quitcmd(UAContext *ua, TREE_CTX *tree) 
816 {
817    return 0;
818 }