3 * Bacula Director -- User Agent Database restore Command
4 * Creates a bootstrap file for restoring files
6 * Kern Sibbald, July MMII
12 Copyright (C) 2002 Kern Sibbald and John Walker
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.
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.
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,
38 /* Imported functions */
39 extern int runcmd(UAContext *ua, char *cmd);
41 /* Imported variables */
42 extern char *uar_list_jobs, *uar_file, *uar_sel_files;
43 extern char *uar_del_temp, *uar_del_temp1, *uar_create_temp;
44 extern char *uar_create_temp1, *uar_last_full, *uar_full;
45 extern char *uar_inc, *uar_list_temp, *uar_sel_jobid_temp;
46 extern char *uar_sel_all_temp1, *uar_sel_fileset;
48 /* Context for insert_tree_handler() */
49 typedef struct s_tree_ctx {
50 TREE_ROOT *root; /* root */
51 TREE_NODE *node; /* current node */
52 TREE_NODE *avail_node; /* unused node last insert */
53 int cnt; /* count for user feedback */
57 typedef struct s_jobids {
66 /* FileIndex entry in bootstrap record */
67 typedef struct s_rbsr_findex {
68 struct s_rbsr_findex *next;
73 /* Restore bootstrap record -- not the real one, but useful here */
74 typedef struct s_rbsr {
75 struct s_rbsr *next; /* next JobId */
76 uint32_t JobId; /* JobId this bsr */
77 uint32_t VolSessionId;
78 uint32_t VolSessionTime;
79 char *VolumeName; /* Volume name */
80 RBSR_FINDEX *fi; /* File indexes this JobId */
83 /* Forward referenced functions */
84 static RBSR *new_bsr();
85 static void free_bsr(RBSR *bsr);
86 static void write_bsr(UAContext *ua, RBSR *bsr, FILE *fd);
87 static int write_bsr_file(UAContext *ua, RBSR *bsr);
88 static void print_bsr(UAContext *ua, RBSR *bsr);
89 static int complete_bsr(UAContext *ua, RBSR *bsr);
90 static int insert_tree_handler(void *ctx, int num_fields, char **row);
91 static void add_findex(RBSR *bsr, uint32_t JobId, int32_t findex);
92 static int last_full_handler(void *ctx, int num_fields, char **row);
93 static int jobid_handler(void *ctx, int num_fields, char **row);
94 static int next_jobid_from_list(char **p, uint32_t *JobId);
95 static int user_select_jobids(UAContext *ua, JobIds *ji);
96 static void user_select_files(TREE_CTX *tree);
97 static int fileset_handler(void *ctx, int num_fields, char **row);
104 int restorecmd(UAContext *ua, char *cmd)
108 JobId_t JobId, last_JobId;
114 JOB *restore_job = NULL;
115 int restore_jobs = 0;
121 memset(&tree, 0, sizeof(TREE_CTX));
122 memset(&ji, 0, sizeof(ji));
124 /* Ensure there is at least one Restore Job */
126 while ( (job = (JOB *)GetNextRes(R_JOB, (RES *)job)) ) {
127 if (job->JobType == JT_RESTORE) {
137 "No Restore Job Resource found. You must create at least\n"
138 "one before running this command.\n"));
143 if (!user_select_jobids(ua, &ji)) {
148 * Build the directory tree
150 tree.root = new_tree(ji.TotalFiles);
151 tree.root->fname = nofname;
153 query = get_pool_memory(PM_MESSAGE);
156 * For display purposes, the same JobId, with different volumes may
157 * appear more than once, however, we only insert it once.
159 for (p=ji.JobIds; next_jobid_from_list(&p, &JobId) > 0; ) {
160 if (JobId == last_JobId) {
161 continue; /* eliminate duplicate JobIds */
164 bsendmsg(ua, _("Building directory tree for JobId %u ...\n"), JobId);
165 Mmsg(&query, uar_sel_files, JobId);
166 if (!db_sql_query(ua->db, query, insert_tree_handler, (void *)&tree)) {
167 bsendmsg(ua, "%s", db_strerror(ua->db));
171 free_pool_memory(query);
173 /* Let the user select which files to restore */
174 user_select_files(&tree);
177 * Walk down through the tree finding all files marked to be
178 * extracted making a bootstrap file.
181 for (TREE_NODE *node=first_tree_node(tree.root); node; node=next_tree_node(node)) {
182 Dmsg2(400, "FI=%d node=0x%x\n", node->FileIndex, node);
184 Dmsg2(400, "type=%d FI=%d\n", node->type, node->FileIndex);
185 add_findex(bsr, node->JobId, node->FileIndex);
189 free_tree(tree.root); /* free the directory tree */
192 complete_bsr(ua, bsr); /* find Vol, SessId, SessTime from JobIds */
193 // print_bsr(ua, bsr);
194 write_bsr_file(ua, bsr);
196 bsendmsg(ua, _("No files selected to restore.\n"));
200 if (restore_jobs == 1) {
203 job = select_restore_job_resource(ua);
206 bsendmsg(ua, _("No Restore Job resource found!\n"));
211 Mmsg(&ua->cmd, "run job=%s client=%s bootstrap=%s/restore.bsr",
212 job->hdr.name, ji.client->hdr.name, working_directory);
214 Mmsg(&ua->cmd, "run job=%s bootstrap=%s/restore.bsr",
215 job->hdr.name, working_directory);
218 Dmsg1(400, "Submitting: %s\n", ua->cmd);
220 parse_command_args(ua);
223 bsendmsg(ua, _("Restore command done.\n"));
228 * The first step in the restore process is for the user to
229 * select a list of JobIds from which he will subsequently
230 * select which files are to be restored.
232 static int user_select_jobids(UAContext *ua, JobIds *ji)
234 char fileset_name[MAX_NAME_LENGTH];
242 "List last 20 Jobs run",
243 "List Jobs where a given File is saved",
244 "Enter list of JobIds to select",
245 "Enter SQL list command",
246 "Select the most recent backup for a client",
250 bsendmsg(ua, _("\nFirst you select one or more JobIds that contain files\n"
251 "to be restored. You will be presented several methods\n"
252 "of specifying the JobIds. Then you will be allowed to\n"
253 "select which files from those JobIds are to be restored.\n\n"));
256 start_prompt(ua, _("To select the JobIds, you have the following choices:\n"));
257 for (int i=0; list[i]; i++) {
258 add_prompt(ua, list[i]);
261 switch (do_prompt(ua, "Select item: ", NULL)) {
264 case 0: /* list last 20 Jobs run */
265 db_list_sql_query(ua->db, uar_list_jobs, prtit, ua, 1);
268 case 1: /* list where a file is saved */
269 if (!get_cmd(ua, _("Enter Filename: "))) {
272 query = get_pool_memory(PM_MESSAGE);
273 Mmsg(&query, uar_file, ua->cmd);
274 db_list_sql_query(ua->db, query, prtit, ua, 1);
275 free_pool_memory(query);
278 case 2: /* enter a list of JobIds */
279 if (!get_cmd(ua, _("Enter JobId(s), comma separated, to restore: "))) {
282 bstrncpy(ji->JobIds, ua->cmd, sizeof(ji->JobIds));
284 case 3: /* Enter an SQL list command */
285 if (!get_cmd(ua, _("Enter SQL list command: "))) {
288 db_list_sql_query(ua->db, ua->cmd, prtit, ua, 1);
291 case 4: /* Select the most recent backups */
292 query = get_pool_memory(PM_MESSAGE);
293 db_sql_query(ua->db, uar_del_temp, NULL, NULL);
294 db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
295 if (!db_sql_query(ua->db, uar_create_temp, NULL, NULL)) {
296 bsendmsg(ua, "%s\n", db_strerror(ua->db));
298 if (!db_sql_query(ua->db, uar_create_temp1, NULL, NULL)) {
299 bsendmsg(ua, "%s\n", db_strerror(ua->db));
304 if (!(ji->client = get_client_resource(ua))) {
311 Mmsg(&query, uar_sel_fileset, ji->client->hdr.name);
312 start_prompt(ua, _("The defined FileSet resources are:\n"));
313 if (!db_sql_query(ua->db, query, fileset_handler, (void *)ua)) {
314 bsendmsg(ua, "%s\n", db_strerror(ua->db));
316 if (do_prompt(ua, _("Select FileSet resource"), fileset_name) < 0) {
317 free_pool_memory(query);
321 strcpy(fsr.FileSet, fileset_name);
322 if (!db_get_fileset_record(ua->db, &fsr)) {
323 bsendmsg(ua, "%s\n", db_strerror(ua->db));
324 free_pool_memory(query);
328 Mmsg(&query, uar_last_full, ji->client->hdr.name, fsr.FileSetId);
329 /* Find JobId of full Backup of system */
330 if (!db_sql_query(ua->db, query, NULL, NULL)) {
331 bsendmsg(ua, "%s\n", db_strerror(ua->db));
333 /* Find all Volumes used by that JobId */
334 if (!db_sql_query(ua->db, uar_full, NULL,NULL)) {
335 bsendmsg(ua, "%s\n", db_strerror(ua->db));
337 /* Note, this is needed as I don't seem to get the callback
338 * from the call just above.
340 if (!db_sql_query(ua->db, uar_sel_all_temp1, last_full_handler, (void *)ji)) {
341 bsendmsg(ua, "%s\n", db_strerror(ua->db));
343 /* Now find all Incremental Jobs */
344 Mmsg(&query, uar_inc, (uint32_t)ji->JobTDate, ji->ClientId, fsr.FileSetId);
345 if (!db_sql_query(ua->db, query, NULL, NULL)) {
346 bsendmsg(ua, "%s\n", db_strerror(ua->db));
348 free_pool_memory(query);
349 db_list_sql_query(ua->db, uar_list_temp, prtit, ua, 1);
351 if (!db_sql_query(ua->db, uar_sel_jobid_temp, jobid_handler, (void *)ji)) {
352 bsendmsg(ua, "%s\n", db_strerror(ua->db));
354 db_sql_query(ua->db, uar_del_temp, NULL, NULL);
355 db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
362 if (*ji->JobIds == 0) {
363 bsendmsg(ua, _("No Jobs selected.\n"));
366 bsendmsg(ua, _("You have selected the following JobId: %s\n"), ji->JobIds);
368 memset(&jr, 0, sizeof(JOB_DBR));
371 for (p=ji->JobIds; ; ) {
372 int stat = next_jobid_from_list(&p, &JobId);
374 bsendmsg(ua, _("Invalid JobId in list.\n"));
381 if (!db_get_job_record(ua->db, &jr)) {
382 bsendmsg(ua, _("Unable to get Job record. ERR=%s\n"), db_strerror(ua->db));
385 ji->TotalFiles += jr.JobFiles;
390 static int next_jobid_from_list(char **p, uint32_t *JobId)
397 for (i=0; i<(int)sizeof(jobid); i++) {
398 if (*q == ',' || *q == 0) {
405 if (jobid[0] == 0 || !is_a_number(jobid)) {
409 *JobId = strtoul(jobid, NULL, 10);
414 * Callback handler make list of JobIds
416 static int jobid_handler(void *ctx, int num_fields, char **row)
418 JobIds *ji = (JobIds *)ctx;
420 if (strlen(ji->JobIds)+strlen(row[0])+2 < sizeof(ji->JobIds)) {
421 if (ji->JobIds[0] != 0) {
422 strcat(ji->JobIds, ",");
424 strcat(ji->JobIds, row[0]);
432 * Callback handler to pickup last Full backup JobId and ClientId
434 static int last_full_handler(void *ctx, int num_fields, char **row)
436 JobIds *ji = (JobIds *)ctx;
438 ji->JobTDate = atoi(row[1]);
439 ji->ClientId = atoi(row[2]);
445 * Callback handler build fileset prompt list
447 static int fileset_handler(void *ctx, int num_fields, char **row)
449 add_prompt((UAContext *)ctx, row[1]);
453 /* Forward referenced commands */
455 static int markcmd(UAContext *ua, TREE_CTX *tree);
456 static int countcmd(UAContext *ua, TREE_CTX *tree);
457 static int findcmd(UAContext *ua, TREE_CTX *tree);
458 static int lscmd(UAContext *ua, TREE_CTX *tree);
459 static int dircmd(UAContext *ua, TREE_CTX *tree);
460 static int helpcmd(UAContext *ua, TREE_CTX *tree);
461 static int cdcmd(UAContext *ua, TREE_CTX *tree);
462 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
463 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
464 static int quitcmd(UAContext *ua, TREE_CTX *tree);
467 struct cmdstruct { char *key; int (*func)(UAContext *ua, TREE_CTX *tree); char *help; };
468 static struct cmdstruct commands[] = {
469 { N_("mark"), markcmd, _("mark file for restoration")},
470 { N_("unmark"), unmarkcmd, _("unmark file for restoration")},
471 { N_("cd"), cdcmd, _("change current directory")},
472 { N_("pwd"), pwdcmd, _("print current working directory")},
473 { N_("ls"), lscmd, _("list current directory")},
474 { N_("dir"), dircmd, _("list current directory")},
475 { N_("count"), countcmd, _("count marked files")},
476 { N_("find"), findcmd, _("find files")},
477 { N_("done"), quitcmd, _("leave file selection mode")},
478 { N_("exit"), quitcmd, _("exit = done")},
479 { N_("help"), helpcmd, _("print help")},
480 { N_("?"), helpcmd, _("print help")},
482 #define comsize (sizeof(commands)/sizeof(struct cmdstruct))
486 * Enter a prompt mode where the user can select/deselect
487 * files to be restored. This is sort of like a mini-shell
488 * that allows "cd", "pwd", "add", "rm", ...
490 static void user_select_files(TREE_CTX *tree)
494 bsendmsg(tree->ua, _(
495 "You are now entering file selection mode where you add and\n"
496 "remove files to be restored. All files are initially added.\n"
497 "Enter done to leave this mode.\n\n"));
499 * Enter interactive command handler allowing selection
500 * of individual files.
502 tree->node = (TREE_NODE *)tree->root;
503 tree_getpath(tree->node, cwd, sizeof(cwd));
504 bsendmsg(tree->ua, _("cwd is: %s\n"), cwd);
506 int found, len, stat, i;
507 if (!get_cmd(tree->ua, "$ ")) {
510 parse_command_args(tree->ua);
511 if (tree->ua->argc == 0) {
515 len = strlen(tree->ua->argk[0]);
517 for (i=0; i<(int)comsize; i++) /* search for command */
518 if (strncasecmp(tree->ua->argk[0], _(commands[i].key), len) == 0) {
519 stat = (*commands[i].func)(tree->ua, tree); /* go execute command */
524 bsendmsg(tree->ua, _("Illegal command. Enter \"done\" to exit.\n"));
533 * Create new FileIndex entry for BSR
535 static RBSR_FINDEX *new_findex()
537 RBSR_FINDEX *fi = (RBSR_FINDEX *)malloc(sizeof(RBSR_FINDEX));
538 memset(fi, 0, sizeof(RBSR_FINDEX));
542 /* Free all BSR FileIndex entries */
543 static void free_findex(RBSR_FINDEX *fi)
546 free_findex(fi->next);
551 static void write_findex(UAContext *ua, RBSR_FINDEX *fi, FILE *fd)
554 if (fi->findex == fi->findex2) {
555 fprintf(fd, "FileIndex=%d\n", fi->findex);
557 fprintf(fd, "FileIndex=%d-%d\n", fi->findex, fi->findex2);
559 write_findex(ua, fi->next, fd);
564 static void print_findex(UAContext *ua, RBSR_FINDEX *fi)
567 if (fi->findex == fi->findex2) {
568 bsendmsg(ua, "FileIndex=%d\n", fi->findex);
570 bsendmsg(ua, "FileIndex=%d-%d\n", fi->findex, fi->findex2);
572 print_findex(ua, fi->next);
576 /* Create a new bootstrap record */
577 static RBSR *new_bsr()
579 RBSR *bsr = (RBSR *)malloc(sizeof(RBSR));
580 memset(bsr, 0, sizeof(RBSR));
584 /* Free the entire BSR */
585 static void free_bsr(RBSR *bsr)
588 free_findex(bsr->fi);
590 if (bsr->VolumeName) {
591 free(bsr->VolumeName);
598 * Complete the BSR by filling in the VolumeName and
599 * VolSessionId and VolSessionTime using the JobId
601 static int complete_bsr(UAContext *ua, RBSR *bsr)
604 POOLMEM *VolumeNames;
607 VolumeNames = get_pool_memory(PM_MESSAGE);
608 memset(&jr, 0, sizeof(jr));
609 jr.JobId = bsr->JobId;
610 if (!db_get_job_record(ua->db, &jr)) {
611 bsendmsg(ua, _("Unable to get Job record. ERR=%s\n"), db_strerror(ua->db));
614 bsr->VolSessionId = jr.VolSessionId;
615 bsr->VolSessionTime = jr.VolSessionTime;
616 if (!db_get_job_volume_names(ua->db, bsr->JobId, &VolumeNames)) {
617 bsendmsg(ua, _("Unable to get Job Volumes. ERR=%s\n"), db_strerror(ua->db));
618 free_pool_memory(VolumeNames);
621 bsr->VolumeName = bstrdup(VolumeNames);
622 free_pool_memory(VolumeNames);
623 return complete_bsr(ua, bsr->next);
629 * Write the bootstrap record to file
631 static int write_bsr_file(UAContext *ua, RBSR *bsr)
634 POOLMEM *fname = get_pool_memory(PM_MESSAGE);
637 Mmsg(&fname, "%s/restore.bsr", working_directory);
638 fd = fopen(fname, "w+");
640 bsendmsg(ua, _("Unable to create bootstrap file %s. ERR=%s\n"),
641 fname, strerror(errno));
642 free_pool_memory(fname);
645 write_bsr(ua, bsr, fd);
648 bsendmsg(ua, _("Bootstrap records written to %s\n"), fname);
649 free_pool_memory(fname);
653 static void write_bsr(UAContext *ua, RBSR *bsr, FILE *fd)
656 if (bsr->VolumeName) {
657 fprintf(fd, "Volume=\"%s\"\n", bsr->VolumeName);
659 fprintf(fd, "VolSessionId=%u\n", bsr->VolSessionId);
660 fprintf(fd, "VolSessionTime=%u\n", bsr->VolSessionTime);
661 write_findex(ua, bsr->fi, fd);
662 write_bsr(ua, bsr->next, fd);
666 static void print_bsr(UAContext *ua, RBSR *bsr)
669 if (bsr->VolumeName) {
670 bsendmsg(ua, "Volume=\"%s\"\n", bsr->VolumeName);
672 bsendmsg(ua, "VolSessionId=%u\n", bsr->VolSessionId);
673 bsendmsg(ua, "VolSessionTime=%u\n", bsr->VolSessionTime);
674 print_findex(ua, bsr->fi);
675 print_bsr(ua, bsr->next);
681 * Add a FileIndex to the list of BootStrap records.
682 * Here we are only dealing with JobId's and the FileIndexes
683 * associated with those JobIds.
685 static void add_findex(RBSR *bsr, uint32_t JobId, int32_t findex)
688 RBSR_FINDEX *fi, *lfi;
691 return; /* probably a dummy directory */
694 if (!bsr->fi) { /* if no FI add one */
695 /* This is the first FileIndex item in the chain */
696 bsr->fi = new_findex();
698 bsr->fi->findex = findex;
699 bsr->fi->findex2 = findex;
702 /* Walk down list of bsrs until we find the JobId */
703 if (bsr->JobId != JobId) {
704 for (nbsr=bsr->next; nbsr; nbsr=nbsr->next) {
705 if (nbsr->JobId == JobId) {
711 if (!nbsr) { /* Must add new JobId */
712 /* Add new JobId at end of chain */
713 for (nbsr=bsr; nbsr->next; nbsr=nbsr->next)
715 nbsr->next = new_bsr();
716 nbsr->next->JobId = JobId;
717 nbsr->next->fi = new_findex();
718 nbsr->next->fi->findex = findex;
719 nbsr->next->fi->findex2 = findex;
725 * At this point, bsr points to bsr containing JobId,
726 * and we are sure that there is at least one fi record.
729 /* Check if this findex is smaller than first item */
730 if (findex < fi->findex) {
731 if ((findex+1) == fi->findex) {
732 fi->findex = findex; /* extend down */
735 fi = new_findex(); /* yes, insert before first item */
737 fi->findex2 = findex;
742 /* Walk down fi chain and find where to insert insert new FileIndex */
743 for ( ; fi; fi=fi->next) {
744 if (findex == (fi->findex2 + 1)) { /* extend up */
746 fi->findex2 = findex;
747 if (fi->next && ((findex+1) == fi->next->findex)) {
749 fi->findex2 = nfi->findex2;
750 fi->next = nfi->next;
755 if (findex < fi->findex) { /* add before */
756 if ((findex+1) == fi->findex) {
764 /* Add to last place found */
767 fi->findex2 = findex;
768 fi->next = lfi->next;
774 * This callback routine is responsible for inserting the
775 * items it gets into the directory tree. For each JobId selected
776 * this routine is called once for each file. We do not allow
777 * duplicate filenames, but instead keep the info from the most
778 * recent file entered (i.e. the JobIds are assumed to be sorted)
780 static int insert_tree_handler(void *ctx, int num_fields, char **row)
782 TREE_CTX *tree = (TREE_CTX *)ctx;
784 TREE_NODE *node, *new_node;
787 strip_trailing_junk(row[1]);
793 sprintf(fname, "%s%s", row[0], row[1]);
794 if (tree->avail_node) {
795 node = tree->avail_node;
797 node = new_tree_node(tree->root, type);
798 tree->avail_node = node;
800 Dmsg2(400, "FI=%d fname=%s\n", node->FileIndex, fname);
801 new_node = insert_tree_node(fname, node, tree->root, NULL);
802 /* Note, if node already exists, save new one for next time */
803 if (new_node != node) {
804 tree->avail_node = node;
806 tree->avail_node = NULL;
808 new_node->FileIndex = atoi(row[2]);
809 new_node->JobId = atoi(row[3]);
810 new_node->type = type;
811 new_node->extract = 1; /* extract all by default */
818 * Set extract to value passed. We recursively walk
819 * down the tree setting all children if the
820 * node is a directory.
822 static void set_extract(TREE_NODE *node, int value)
826 node->extract = value;
827 if (node->type != TN_FILE) {
828 for (n=node->child; n; n=n->sibling) {
829 set_extract(n, value);
834 static int markcmd(UAContext *ua, TREE_CTX *tree)
840 if (!tree->node->child) {
843 for (node = tree->node->child; node; node=node->sibling) {
844 if (fnmatch(ua->argk[1], node->fname, 0) == 0) {
845 set_extract(node, 1);
851 static int countcmd(UAContext *ua, TREE_CTX *tree)
856 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
857 if (node->type != TN_NEWDIR) {
864 bsendmsg(ua, "%d total files. %d marked for restoration.\n", total, extract);
868 static int findcmd(UAContext *ua, TREE_CTX *tree)
873 bsendmsg(ua, _("No file specification given.\n"));
877 for (int i=1; i < ua->argc; i++) {
878 for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
879 if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
880 tree_getpath(node, cwd, sizeof(cwd));
881 bsendmsg(ua, "%s%s\n", node->extract?"*":"", cwd);
890 static int lscmd(UAContext *ua, TREE_CTX *tree)
894 if (!tree->node->child) {
897 for (node = tree->node->child; node; node=node->sibling) {
898 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
899 bsendmsg(ua, "%s%s%s\n", node->extract?"*":"", node->fname,
900 (node->type==TN_DIR||node->type==TN_NEWDIR)?"/":"");
906 extern char *getuser(uid_t uid);
907 extern char *getgroup(gid_t gid);
909 static void ls_output(char *buf, char *fname, struct stat *statp)
914 p = encode_mode(statp->st_mode, buf);
915 n = sprintf(p, " %2d ", (uint32_t)statp->st_nlink);
917 n = sprintf(p, "%-8.8s %-8.8s", getuser(statp->st_uid), getgroup(statp->st_gid));
919 n = sprintf(p, "%8ld ", statp->st_size);
921 p = encode_time(statp->st_ctime, p);
931 * Like ls command, but give more detail on each file
933 static int dircmd(UAContext *ua, TREE_CTX *tree)
941 if (!tree->node->child) {
944 for (node = tree->node->child; node; node=node->sibling) {
945 if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
946 tree_getpath(node, cwd, sizeof(cwd));
948 fdbr.JobId = node->JobId;
949 if (db_get_file_attributes_record(ua->db, cwd, &fdbr)) {
950 decode_stat(fdbr.LStat, &statp); /* decode stat pkt */
951 ls_output(buf, cwd, &statp);
952 bsendmsg(ua, "%s\n", buf);
960 static int helpcmd(UAContext *ua, TREE_CTX *tree)
965 bsendmsg(ua, _(" Command Description\n ======= ===========\n"));
966 for (i=0; i<comsize; i++) {
967 bsendmsg(ua, _(" %-10s %s\n"), _(commands[i].key), _(commands[i].help));
973 static int cdcmd(UAContext *ua, TREE_CTX *tree)
981 node = tree_cwd(ua->argk[1], tree->root, tree->node);
983 bsendmsg(ua, _("Invalid path given.\n"));
987 tree_getpath(tree->node, cwd, sizeof(cwd));
988 bsendmsg(ua, _("cwd is: %s\n"), cwd);
992 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
995 tree_getpath(tree->node, cwd, sizeof(cwd));
996 bsendmsg(ua, _("cwd is: %s\n"), cwd);
1001 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
1007 if (!tree->node->child) {
1010 for (node = tree->node->child; node; node=node->sibling) {
1011 if (fnmatch(ua->argk[1], node->fname, 0) == 0) {
1012 set_extract(node, 0);
1018 static int quitcmd(UAContext *ua, TREE_CTX *tree)