]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/fd_cmds.c
Minor runscript tweaks
[bacula/bacula] / bacula / src / dird / fd_cmds.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2000-2007 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU General Public
10    License as published by the Free Software Foundation and included
11    in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of John Walker.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28 /*
29  *
30  *   Bacula Director -- fd_cmds.c -- send commands to File daemon
31  *
32  *     Kern Sibbald, October MM
33  *
34  *    This routine is run as a separate thread.  There may be more
35  *    work to be done to make it totally reentrant!!!!
36  *
37  *  Utility functions for sending info to File Daemon.
38  *   These functions are used by both backup and verify.
39  *
40  *   Version $Id$
41  */
42
43 #include "bacula.h"
44 #include "dird.h"
45 #include "findlib/find.h"
46
47 const int dbglvl = 400;
48
49 /* Commands sent to File daemon */
50 static char filesetcmd[]  = "fileset%s\n"; /* set full fileset */
51 static char jobcmd[]      = "JobId=%s Job=%s SDid=%u SDtime=%u Authorization=%s\n";
52 /* Note, mtime_only is not used here -- implemented as file option */
53 static char levelcmd[]    = "level = %s%s mtime_only=%d\n";
54 static char runscript[]   = "Run OnSuccess=%u OnFailure=%u AbortOnError=%u When=%u Command=%s\n";
55 static char runbeforenow[]= "RunBeforeNow\n";
56
57 /* Responses received from File daemon */
58 static char OKinc[]          = "2000 OK include\n";
59 static char OKjob[]          = "2000 OK Job";
60 static char OKlevel[]        = "2000 OK level\n";
61 static char OKRunScript[]    = "2000 OK RunScript\n";
62 static char OKRunBeforeNow[] = "2000 OK RunBeforeNow\n";
63
64 /* Forward referenced functions */
65
66 /* External functions */
67 extern DIRRES *director;
68 extern int FDConnectTimeout;
69
70 #define INC_LIST 0
71 #define EXC_LIST 1
72
73 /*
74  * Open connection with File daemon.
75  * Try connecting every retry_interval (default 10 sec), and
76  *   give up after max_retry_time (default 30 mins).
77  */
78
79 int connect_to_file_daemon(JCR *jcr, int retry_interval, int max_retry_time,
80                            int verbose)
81 {
82    BSOCK   *fd;
83    char ed1[30];
84    utime_t heart_beat;
85
86    if (jcr->client->heartbeat_interval) {
87       heart_beat = jcr->client->heartbeat_interval;
88    } else {           
89       heart_beat = director->heartbeat_interval;
90    }
91
92    if (!jcr->file_bsock) {
93       fd = bnet_connect(jcr, retry_interval, max_retry_time, heart_beat,
94            _("File daemon"), jcr->client->address,
95            NULL, jcr->client->FDport, verbose);
96       if (fd == NULL) {
97          set_jcr_job_status(jcr, JS_ErrorTerminated);
98          return 0;
99       }
100       Dmsg0(10, "Opened connection with File daemon\n");
101    } else {
102       fd = jcr->file_bsock;           /* use existing connection */
103    }
104    fd->res = (RES *)jcr->client;      /* save resource in BSOCK */
105    jcr->file_bsock = fd;
106    set_jcr_job_status(jcr, JS_Running);
107
108    if (!authenticate_file_daemon(jcr)) {
109       set_jcr_job_status(jcr, JS_ErrorTerminated);
110       return 0;
111    }
112
113    /*
114     * Now send JobId and authorization key
115     */
116    bnet_fsend(fd, jobcmd, edit_int64(jcr->JobId, ed1), jcr->Job, jcr->VolSessionId,
117       jcr->VolSessionTime, jcr->sd_auth_key);
118    if (strcmp(jcr->sd_auth_key, "dummy") != 0) {
119       memset(jcr->sd_auth_key, 0, strlen(jcr->sd_auth_key));
120    }
121    Dmsg1(100, ">filed: %s", fd->msg);
122    if (bget_dirmsg(fd) > 0) {
123        Dmsg1(110, "<filed: %s", fd->msg);
124        if (strncmp(fd->msg, OKjob, strlen(OKjob)) != 0) {
125           Jmsg(jcr, M_FATAL, 0, _("File daemon \"%s\" rejected Job command: %s\n"),
126              jcr->client->hdr.name, fd->msg);
127           set_jcr_job_status(jcr, JS_ErrorTerminated);
128           return 0;
129        } else if (jcr->db) {
130           CLIENT_DBR cr;
131           memset(&cr, 0, sizeof(cr));
132           bstrncpy(cr.Name, jcr->client->hdr.name, sizeof(cr.Name));
133           cr.AutoPrune = jcr->client->AutoPrune;
134           cr.FileRetention = jcr->client->FileRetention;
135           cr.JobRetention = jcr->client->JobRetention;
136           bstrncpy(cr.Uname, fd->msg+strlen(OKjob)+1, sizeof(cr.Uname));
137           if (!db_update_client_record(jcr, jcr->db, &cr)) {
138              Jmsg(jcr, M_WARNING, 0, _("Error updating Client record. ERR=%s\n"),
139                 db_strerror(jcr->db));
140           }
141        }
142    } else {
143       Jmsg(jcr, M_FATAL, 0, _("FD gave bad response to JobId command: %s\n"),
144          bnet_strerror(fd));
145       set_jcr_job_status(jcr, JS_ErrorTerminated);
146       return 0;
147    }
148    return 1;
149 }
150
151 /*
152  * This subroutine edits the last job start time into a
153  *   "since=date/time" buffer that is returned in the
154  *   variable since.  This is used for display purposes in
155  *   the job report.  The time in jcr->stime is later
156  *   passed to tell the File daemon what to do.
157  */
158 void get_level_since_time(JCR *jcr, char *since, int since_len)
159 {
160    int JobLevel;
161
162    since[0] = 0;
163    if (jcr->cloned) {
164       if (jcr->stime && jcr->stime[0]) {
165          bstrncpy(since, _(", since="), since_len);
166          bstrncat(since, jcr->stime, since_len);
167       }
168       return;
169    }
170    if (!jcr->stime) {
171       jcr->stime = get_pool_memory(PM_MESSAGE);
172    } 
173    jcr->stime[0] = 0;
174    /* Lookup the last FULL backup job to get the time/date for a
175     * differential or incremental save.
176     */
177    switch (jcr->JobLevel) {
178    case L_DIFFERENTIAL:
179    case L_INCREMENTAL:
180       /* Look up start time of last job */
181       jcr->jr.JobId = 0;     /* flag for db_find_job_start time */
182       if (!db_find_job_start_time(jcr, jcr->db, &jcr->jr, &jcr->stime)) {
183          /* No job found, so upgrade this one to Full */
184          Jmsg(jcr, M_INFO, 0, "%s", db_strerror(jcr->db));
185          Jmsg(jcr, M_INFO, 0, _("No prior or suitable Full backup found in catalog. Doing FULL backup.\n"));
186          bsnprintf(since, since_len, _(" (upgraded from %s)"),
187             level_to_str(jcr->JobLevel));
188          jcr->JobLevel = jcr->jr.JobLevel = L_FULL;
189       } else {
190          if (jcr->job->rerun_failed_levels) {
191             if (db_find_failed_job_since(jcr, jcr->db, &jcr->jr, jcr->stime, JobLevel)) {
192                Jmsg(jcr, M_INFO, 0, _("Prior failed job found in catalog. Upgrading to %s.\n"),
193                   level_to_str(JobLevel));
194                bsnprintf(since, since_len, _(" (upgraded from %s)"),
195                   level_to_str(jcr->JobLevel));
196                jcr->JobLevel = jcr->jr.JobLevel = JobLevel;
197                jcr->jr.JobId = jcr->JobId;
198                break;
199             }
200          }
201          bstrncpy(since, _(", since="), since_len);
202          bstrncat(since, jcr->stime, since_len);
203       }
204       jcr->jr.JobId = jcr->JobId;
205       break;
206    }
207    Dmsg2(100, "Level=%c last start time=%s\n", jcr->JobLevel, jcr->stime);
208 }
209
210 static void send_since_time(JCR *jcr)
211 {
212    BSOCK   *fd = jcr->file_bsock;
213    utime_t stime;
214    char ed1[50];
215
216    stime = str_to_utime(jcr->stime);
217    bnet_fsend(fd, levelcmd, NT_("since_utime "), edit_uint64(stime, ed1), 0);
218    while (bget_dirmsg(fd) >= 0) {  /* allow him to poll us to sync clocks */
219       Jmsg(jcr, M_INFO, 0, "%s\n", fd->msg);
220    }
221 }
222
223
224 /*
225  * Send level command to FD.
226  * Used for backup jobs and estimate command.
227  */
228 bool send_level_command(JCR *jcr)
229 {
230    BSOCK   *fd = jcr->file_bsock;
231    /*
232     * Send Level command to File daemon
233     */
234    switch (jcr->JobLevel) {
235    case L_BASE:
236       bnet_fsend(fd, levelcmd, "base", " ", 0);
237       break;
238    /* L_NONE is the console, sending something off to the FD */
239    case L_NONE:
240    case L_FULL:
241       bnet_fsend(fd, levelcmd, "full", " ", 0);
242       break;
243    case L_DIFFERENTIAL:
244       bnet_fsend(fd, levelcmd, "differential", " ", 0);
245       send_since_time(jcr);
246       break;
247    case L_INCREMENTAL:
248       bnet_fsend(fd, levelcmd, "incremental", " ", 0);
249       send_since_time(jcr);
250       break;
251    case L_SINCE:
252    default:
253       Jmsg2(jcr, M_FATAL, 0, _("Unimplemented backup level %d %c\n"),
254          jcr->JobLevel, jcr->JobLevel);
255       return 0;
256    }
257    Dmsg1(120, ">filed: %s", fd->msg);
258    if (!response(jcr, fd, OKlevel, "Level", DISPLAY_ERROR)) {
259       return 0;
260    }
261    return 1;
262 }
263
264 /*
265  * Send either an Included or an Excluded list to FD
266  */
267 static bool send_fileset(JCR *jcr)
268 {
269    FILESET *fileset = jcr->fileset;
270    BSOCK   *fd = jcr->file_bsock;
271    int num;
272    bool include = true;
273
274    for ( ;; ) {
275       if (include) {
276          num = fileset->num_includes;
277       } else {
278          num = fileset->num_excludes;
279       }
280       for (int i=0; i<num; i++) {
281          BPIPE *bpipe;
282          FILE *ffd;
283          char buf[2000];
284          char *p;
285          int optlen, stat;
286          INCEXE *ie;
287          int j, k;
288
289          if (include) {
290             ie = fileset->include_items[i];
291             bnet_fsend(fd, "I\n");
292          } else {
293             ie = fileset->exclude_items[i];
294             bnet_fsend(fd, "E\n");
295          }
296          for (j=0; j<ie->num_opts; j++) {
297             FOPTS *fo = ie->opts_list[j];
298             bnet_fsend(fd, "O %s\n", fo->opts);
299
300             bool enhanced_wild = false;
301             for (k=0; fo->opts[k]!='\0'; k++) {
302                if (fo->opts[k]=='W') {
303                   enhanced_wild = true;
304                   break;
305                }
306             }
307
308             for (k=0; k<fo->regex.size(); k++) {
309                bnet_fsend(fd, "R %s\n", fo->regex.get(k));
310             }
311             for (k=0; k<fo->regexdir.size(); k++) {
312                bnet_fsend(fd, "RD %s\n", fo->regexdir.get(k));
313             }
314             for (k=0; k<fo->regexfile.size(); k++) {
315                bnet_fsend(fd, "RF %s\n", fo->regexfile.get(k));
316             }
317             for (k=0; k<fo->wild.size(); k++) {
318                bnet_fsend(fd, "W %s\n", fo->wild.get(k));
319             }
320             for (k=0; k<fo->wilddir.size(); k++) {
321                bnet_fsend(fd, "WD %s\n", fo->wilddir.get(k));
322             }
323             for (k=0; k<fo->wildfile.size(); k++) {
324                bnet_fsend(fd, "WF %s\n", fo->wildfile.get(k));
325             }
326             for (k=0; k<fo->wildbase.size(); k++) {
327                bnet_fsend(fd, "W%c %s\n", enhanced_wild ? 'B' : 'F', fo->wildbase.get(k));
328             }
329             for (k=0; k<fo->base.size(); k++) {
330                bnet_fsend(fd, "B %s\n", fo->base.get(k));
331             }
332             for (k=0; k<fo->fstype.size(); k++) {
333                bnet_fsend(fd, "X %s\n", fo->fstype.get(k));
334             }
335             for (k=0; k<fo->drivetype.size(); k++) {
336                bnet_fsend(fd, "XD %s\n", fo->drivetype.get(k));
337             }
338             if (fo->reader) {
339                bnet_fsend(fd, "D %s\n", fo->reader);
340             }
341             if (fo->writer) {
342                bnet_fsend(fd, "T %s\n", fo->writer);
343             }
344             bnet_fsend(fd, "N\n");
345          }
346
347          for (j=0; j<ie->name_list.size(); j++) {
348             p = (char *)ie->name_list.get(j);
349             switch (*p) {
350             case '|':
351                p++;                      /* skip over the | */
352                fd->msg = edit_job_codes(jcr, fd->msg, p, "");
353                bpipe = open_bpipe(fd->msg, 0, "r");
354                if (!bpipe) {
355                   berrno be;
356                   Jmsg(jcr, M_FATAL, 0, _("Cannot run program: %s. ERR=%s\n"),
357                      p, be.bstrerror());
358                   goto bail_out;
359                }
360                bstrncpy(buf, "F ", sizeof(buf));
361                Dmsg1(500, "Opts=%s\n", buf);
362                optlen = strlen(buf);
363                while (fgets(buf+optlen, sizeof(buf)-optlen, bpipe->rfd)) {
364                   fd->msglen = Mmsg(fd->msg, "%s", buf);
365                   Dmsg2(500, "Inc/exc len=%d: %s", fd->msglen, fd->msg);
366                   if (!bnet_send(fd)) {
367                      Jmsg(jcr, M_FATAL, 0, _(">filed: write error on socket\n"));
368                      goto bail_out;
369                   }
370                }
371                if ((stat=close_bpipe(bpipe)) != 0) {
372                   berrno be;
373                   Jmsg(jcr, M_FATAL, 0, _("Error running program: %s. ERR=%s\n"),
374                      p, be.bstrerror(stat));
375                   goto bail_out;
376                }
377                break;
378             case '<':
379                p++;                      /* skip over < */
380                if ((ffd = fopen(p, "rb")) == NULL) {
381                   berrno be;
382                   Jmsg(jcr, M_FATAL, 0, _("Cannot open included file: %s. ERR=%s\n"),
383                      p, be.bstrerror());
384                   goto bail_out;
385                }
386                bstrncpy(buf, "F ", sizeof(buf));
387                Dmsg1(500, "Opts=%s\n", buf);
388                optlen = strlen(buf);
389                while (fgets(buf+optlen, sizeof(buf)-optlen, ffd)) {
390                   fd->msglen = Mmsg(fd->msg, "%s", buf);
391                   if (!bnet_send(fd)) {
392                      Jmsg(jcr, M_FATAL, 0, _(">filed: write error on socket\n"));
393                      goto bail_out;
394                   }
395                }
396                fclose(ffd);
397                break;
398             case '\\':
399                p++;                      /* skip over \ */
400                /* Note, fall through wanted */
401             default:
402                pm_strcpy(fd->msg, "F ");
403                fd->msglen = pm_strcat(fd->msg, p);
404                Dmsg1(500, "Inc/Exc name=%s\n", fd->msg);
405                if (!bnet_send(fd)) {
406                   Jmsg(jcr, M_FATAL, 0, _(">filed: write error on socket\n"));
407                   goto bail_out;
408                }
409                break;
410             }
411          }
412          bnet_fsend(fd, "N\n");
413       }
414       if (!include) {                 /* If we just did excludes */
415          break;                       /*   all done */
416       }
417       include = false;                /* Now do excludes */
418    }
419
420    bnet_sig(fd, BNET_EOD);            /* end of data */
421    if (!response(jcr, fd, OKinc, "Include", DISPLAY_ERROR)) {
422       goto bail_out;
423    }
424    return true;
425
426 bail_out:
427    set_jcr_job_status(jcr, JS_ErrorTerminated);
428    return false;
429
430 }
431
432
433 /*
434  * Send include list to File daemon
435  */
436 bool send_include_list(JCR *jcr)
437 {
438    BSOCK *fd = jcr->file_bsock;
439    if (jcr->fileset->new_include) {
440       bnet_fsend(fd, filesetcmd, jcr->fileset->enable_vss ? " vss=1" : "");
441       return send_fileset(jcr);
442    }
443    return true;
444 }
445
446
447 /*
448  * Send exclude list to File daemon
449  *   Under the new scheme, the Exclude list
450  *   is part of the FileSet sent with the
451  *   "include_list" above.
452  */
453 bool send_exclude_list(JCR *jcr)
454 {
455    return true;
456 }
457
458
459 /*
460  * Send bootstrap file if any to the socket given (FD or SD).
461  *  This is used for restore, verify VolumeToCatalog, and
462  *  for migration.
463  */
464 bool send_bootstrap_file(JCR *jcr, BSOCK *sock)
465 {
466    FILE *bs;
467    char buf[1000];
468    const char *bootstrap = "bootstrap\n";
469
470    Dmsg1(400, "send_bootstrap_file: %s\n", jcr->RestoreBootstrap);
471    if (!jcr->RestoreBootstrap) {
472       return true;
473    }
474    bs = fopen(jcr->RestoreBootstrap, "rb");
475    if (!bs) {
476       berrno be;
477       Jmsg(jcr, M_FATAL, 0, _("Could not open bootstrap file %s: ERR=%s\n"),
478          jcr->RestoreBootstrap, be.bstrerror());
479       set_jcr_job_status(jcr, JS_ErrorTerminated);
480       return false;
481    }
482    bnet_fsend(sock, bootstrap);
483    while (fgets(buf, sizeof(buf), bs)) {
484       bnet_fsend(sock, "%s", buf);
485    }
486    bnet_sig(sock, BNET_EOD);
487    fclose(bs);
488    if (jcr->unlink_bsr) {
489       unlink(jcr->RestoreBootstrap);
490       jcr->unlink_bsr = false;
491    }                         
492    return true;
493 }
494
495 /* TODO: drop this with runscript.old_proto in bacula 1.42 */
496 static char runbefore[]   = "RunBeforeJob %s\n";
497 static char runafter[]    = "RunAfterJob %s\n";
498 static char OKRunBefore[] = "2000 OK RunBefore\n";
499 static char OKRunAfter[]  = "2000 OK RunAfter\n";
500
501 int send_runscript_with_old_proto(JCR *jcr, int when, POOLMEM *msg)
502 {
503    int ret;
504    Dmsg1(120, "bdird: sending old runcommand to fd '%s'\n",msg);
505    if (when & SCRIPT_Before) {
506       bnet_fsend(jcr->file_bsock, runbefore, msg);
507       ret = response(jcr, jcr->file_bsock, OKRunBefore, "ClientRunBeforeJob", DISPLAY_ERROR);
508    } else {
509       bnet_fsend(jcr->file_bsock, runafter, msg);
510       ret = response(jcr, jcr->file_bsock, OKRunAfter, "ClientRunAfterJob", DISPLAY_ERROR);
511    }
512    return ret;
513 } /* END OF TODO */
514
515 /*
516  * Send RunScripts to File daemon
517  * 1) We send all runscript to FD, they can be executed Before, After, or twice
518  * 2) Then, we send a "RunBeforeNow" command to the FD to tell him to do the
519  *    first run_script() call. (ie ClientRunBeforeJob)
520  */
521 int send_runscripts_commands(JCR *jcr)
522 {
523    POOLMEM *msg = get_pool_memory(PM_FNAME);
524    BSOCK *fd = jcr->file_bsock;
525    RUNSCRIPT *cmd;
526    bool launch_before_cmd = false;
527    POOLMEM *ehost = get_pool_memory(PM_FNAME);
528    int result;
529
530    Dmsg0(120, "bdird: sending runscripts to fd\n");
531    
532    foreach_alist(cmd, jcr->job->RunScripts) {
533       if (cmd->can_run_at_level(jcr->JobLevel) && cmd->target) {
534          ehost = edit_job_codes(jcr, ehost, cmd->target, "");
535          Dmsg2(200, "bdird: runscript %s -> %s\n", cmd->target, ehost);
536
537          if (strcmp(ehost, jcr->client->name()) == 0) {
538             pm_strcpy(msg, cmd->command);
539             bash_spaces(msg);
540
541             Dmsg1(120, "bdird: sending runscripts to fd '%s'\n", cmd->command);
542             
543             /* TODO: remove this with bacula 1.42 */
544             if (cmd->old_proto) {
545                result = send_runscript_with_old_proto(jcr, cmd->when, msg);
546
547             } else {
548                bnet_fsend(fd, runscript, cmd->on_success, 
549                                          cmd->on_failure,
550                                          cmd->fail_on_error,
551                                          cmd->when,
552                                          msg);
553
554                result = response(jcr, fd, OKRunScript, "RunScript", DISPLAY_ERROR);
555                launch_before_cmd = true;
556             }
557             
558             if (!result) {
559                goto bail_out;
560             }
561          }
562          /* TODO : we have to play with other client */
563          /*
564            else {
565            send command to an other client
566            }
567          */
568       }        
569    } 
570
571    /* Tell the FD to execute the ClientRunBeforeJob */
572    if (launch_before_cmd) {
573       bnet_fsend(fd, runbeforenow);
574       if (!response(jcr, fd, OKRunBeforeNow, "RunBeforeNow", DISPLAY_ERROR)) {
575         goto bail_out;
576       }
577    }
578    free_pool_memory(msg);
579    free_pool_memory(ehost);
580    return 1;
581
582 bail_out:
583    Jmsg(jcr, M_FATAL, 0, _("Client \"%s\" RunScript failed.\n"), ehost);
584    free_pool_memory(msg);
585    free_pool_memory(ehost);
586    return 0;
587 }
588
589
590
591 /*
592  * Read the attributes from the File daemon for
593  * a Verify job and store them in the catalog.
594  */
595 int get_attributes_and_put_in_catalog(JCR *jcr)
596 {
597    BSOCK   *fd;
598    int n = 0;
599    ATTR_DBR *ar = NULL;
600    char digest[MAXSTRING];
601
602    fd = jcr->file_bsock;
603    jcr->jr.FirstIndex = 1;
604    jcr->FileIndex = 0;
605    /* Start transaction allocates jcr->attr and jcr->ar if needed */
606    db_start_transaction(jcr, jcr->db);     /* start transaction if not already open */
607    ar = jcr->ar;
608
609    Dmsg0(120, "bdird: waiting to receive file attributes\n");
610    /* Pickup file attributes and digest */
611    while (!fd->errors && (n = bget_dirmsg(fd)) > 0) {
612       uint32_t file_index;
613       int stream, len;
614       char *p, *fn;
615       char Digest[MAXSTRING];      /* either Verify opts or MD5/SHA1 digest */
616
617       jcr->fname = check_pool_memory_size(jcr->fname, fd->msglen);
618       if ((len = sscanf(fd->msg, "%ld %d %s", &file_index, &stream, Digest)) != 3) {
619          Jmsg(jcr, M_FATAL, 0, _("<filed: bad attributes, expected 3 fields got %d\n"
620 "msglen=%d msg=%s\n"), len, fd->msglen, fd->msg);
621          set_jcr_job_status(jcr, JS_ErrorTerminated);
622          return 0;
623       }
624       p = fd->msg;
625       /* The following three fields were sscanf'ed above so skip them */
626       skip_nonspaces(&p);             /* skip FileIndex */
627       skip_spaces(&p);
628       skip_nonspaces(&p);             /* skip Stream */
629       skip_spaces(&p);
630       skip_nonspaces(&p);             /* skip Opts_Digest */
631       p++;                            /* skip space */
632       Dmsg1(dbglvl, "Stream=%d\n", stream);
633       if (stream == STREAM_UNIX_ATTRIBUTES || stream == STREAM_UNIX_ATTRIBUTES_EX) {
634          if (jcr->cached_attribute) {
635             Dmsg3(dbglvl, "Cached attr. Stream=%d fname=%s\n", ar->Stream, ar->fname,
636                ar->attr);
637             if (!db_create_file_attributes_record(jcr, jcr->db, ar)) {
638                Jmsg1(jcr, M_FATAL, 0, _("Attribute create error. %s"), db_strerror(jcr->db));
639             }
640          }
641          /* Any cached attr is flushed so we can reuse jcr->attr and jcr->ar */
642          fn = jcr->fname;
643          while (*p != 0) {
644             *fn++ = *p++;                /* copy filename */
645          }
646          *fn = *p++;                     /* term filename and point p to attribs */
647          pm_strcpy(jcr->attr, p);        /* save attributes */
648          jcr->JobFiles++;
649          jcr->FileIndex = file_index;
650          ar->attr = jcr->attr;
651          ar->fname = jcr->fname;
652          ar->FileIndex = file_index;
653          ar->Stream = stream;
654          ar->link = NULL;
655          ar->JobId = jcr->JobId;
656          ar->ClientId = jcr->ClientId;
657          ar->PathId = 0;
658          ar->FilenameId = 0;
659          ar->Digest = NULL;
660          ar->DigestType = CRYPTO_DIGEST_NONE;
661          jcr->cached_attribute = true;
662
663          Dmsg2(dbglvl, "dird<filed: stream=%d %s\n", stream, jcr->fname);
664          Dmsg1(dbglvl, "dird<filed: attr=%s\n", ar->attr);
665          jcr->FileId = ar->FileId;
666       /*
667        * First, get STREAM_UNIX_ATTRIBUTES and fill ATTR_DBR structure
668        * Next, we CAN have a CRYPTO_DIGEST, so we fill ATTR_DBR with it (or not)
669        * When we get a new STREAM_UNIX_ATTRIBUTES, we known that we can add file to the catalog
670        * At the end, we have to add the last file
671        */
672       } else if (crypto_digest_stream_type(stream) != CRYPTO_DIGEST_NONE) {
673          if (jcr->FileIndex != (uint32_t)file_index) {
674             Jmsg3(jcr, M_ERROR, 0, _("%s index %d not same as attributes %d\n"),
675                stream_to_ascii(stream), file_index, jcr->FileIndex);
676             continue;
677          }
678          ar->Digest = digest;
679          ar->DigestType = crypto_digest_stream_type(stream);
680          db_escape_string(digest, Digest, strlen(Digest));
681          Dmsg4(dbglvl, "stream=%d DigestLen=%d Digest=%s type=%d\n", stream,
682                strlen(digest), digest, ar->DigestType);
683       }
684       jcr->jr.JobFiles = jcr->JobFiles = file_index;
685       jcr->jr.LastIndex = file_index;
686    }
687    if (is_bnet_error(fd)) {
688       Jmsg1(jcr, M_FATAL, 0, _("<filed: Network error getting attributes. ERR=%s\n"),
689             fd->bstrerror());
690       return 0;
691    }
692    if (jcr->cached_attribute) {
693       Dmsg3(dbglvl, "Cached attr with digest. Stream=%d fname=%s attr=%s\n", ar->Stream,            
694          ar->fname, ar->attr);
695       if (!db_create_file_attributes_record(jcr, jcr->db, ar)) {
696          Jmsg1(jcr, M_FATAL, 0, _("Attribute create error. %s"), db_strerror(jcr->db));
697       }
698       jcr->cached_attribute = false; 
699    }
700    set_jcr_job_status(jcr, JS_Terminated);
701    return 1;
702 }