1 /*
2 Bacula(R) - The Network Backup Solution
3
4 Copyright (C) 2000-2020 Kern Sibbald
5
6 The original author of Bacula is Kern Sibbald, with contributions
7 from many others, a complete list can be found in the file AUTHORS.
8
9 You may use this file and others of this release according to the
10 license defined in the LICENSE file, which includes the Affero General
11 Public License, v3.0 ("AGPLv3") and some additional permissions and
12 terms pursuant to its AGPLv3 Section 7.
13
14 This notice must be preserved when any source code is
15 conveyed and/or propagated.
16
17 Bacula(R) is a registered trademark of Kern Sibbald.
18 */
19
20 /*
21 * Routines for writing to a file from the Cloud device.
22 *
23 * This is for testing purposes only.
24 #
25 * NOTE!!! This cloud driver is not compatible with
26 * any disk-changer script for changing Volumes.
27 * It does however work with Bacula Virtual autochangers.
28 *
29 * Written by Kern Sibbald, May MMXVI
30 *
31 */
32
33 #include "file_driver.h"
34
35 static const int64_t dbglvl = DT_CLOUD|50;
36
37 #include <fcntl.h>
38
39 /* Imported functions */
40 const char *mode_to_str(int mode);
41 int breaddir(DIR *dirp, POOLMEM *&dname);
42
43 /* Forward referenced functions */
44
45 /* Const and Static definitions */
46
47 /*
48 * Put a cache object into the cloud (i.e. local disk)
49 * or visa-versa.
50 */
put_object(transfer * xfer,const char * in_fname,const char * out_fname,bwlimit * limit)51 bool file_driver::put_object(transfer *xfer, const char *in_fname, const char *out_fname, bwlimit *limit)
52 {
53 struct stat statbuf;
54 char *p, *f;
55 char save_separator;
56 ssize_t rbytes, wbytes;
57 uint32_t read_len;
58 int64_t obj_len;
59 FILE *infile=NULL, *outfile=NULL;
60 POOLMEM *buf = get_memory(buf_len);
61
62 Enter(dbglvl);
63 Dmsg2(dbglvl, "Put from: %s to %s\n", in_fname, out_fname);
64
65 /*
66 * First work on output file
67 */
68 /* Split out_fname into path + file */
69 for (p=f=const_cast<char*>(out_fname); *p; p++) {
70 if (IsPathSeparator(*p)) {
71 f = p; /* set pos of last slash */
72 }
73 }
74 if (!IsPathSeparator(*f)) { /* did we find a slash? */
75 Mmsg1(xfer->m_message, "Could not find path name for output file: %s\n", out_fname);
76 goto get_out;
77 }
78 save_separator = *f;
79 *f = 0; /* terminate path */
80
81 /* const_cast should not be necessary here but is due the makedir interface */
82 if (!makedir(NULL, const_cast<char*>(out_fname), 0740)) {
83 Mmsg1(xfer->m_message, "Could not makedir output directory: %s\n", out_fname);
84 *f = save_separator;
85 goto get_out;
86 }
87 *f = save_separator;
88
89 if (lstat(out_fname, &statbuf) == -1) {
90 outfile = bfopen(out_fname, "w");
91 } else {
92 /* append to existing file */
93 outfile = bfopen(out_fname, "r+");
94 }
95
96 if (!outfile) {
97 berrno be;
98 Mmsg2(xfer->m_message, "Could not open output file %s. ERR=%s\n",
99 out_fname, be.bstrerror());
100 goto get_out;
101 }
102
103 /*
104 * Now work on input file
105 */
106 if (lstat(in_fname, &statbuf) == -1) {
107 berrno be;
108 Mmsg2(xfer->m_message, "Failed to stat input file %s. ERR=%s\n",
109 in_fname, be.bstrerror());
110 goto get_out;
111 }
112
113 obj_len = statbuf.st_size;
114 Dmsg1(dbglvl, "Object length to copy is: %lld bytes.\n", obj_len);
115 if (obj_len == 0) { /* Not yet created nothing to do */
116 goto get_out;
117 }
118
119 infile = bfopen(in_fname, "r");
120
121 if (!infile) {
122 berrno be;
123 Mmsg2(xfer->m_message, "Failed to open input file %s. ERR=%s\n",
124 in_fname, be.bstrerror());
125 goto get_out;
126 }
127
128 while (obj_len > 0) {
129 if (xfer->is_canceled()) {
130 Mmsg(xfer->m_message, "Job is canceled.\n");
131 goto get_out;
132 }
133 read_len = (obj_len > buf_len) ? buf_len : obj_len;
134 Dmsg3(dbglvl+10, "obj_len=%d buf_len=%d read_len=%d\n", obj_len, buf_len, read_len);
135 rbytes = fread(buf, 1, read_len, infile);
136 Dmsg1(dbglvl+10, "Read %d bytes.\n", rbytes);
137 if (rbytes <= 0) {
138 berrno be;
139 Mmsg2(xfer->m_message, "Error reading input file %s. ERR=%s\n",
140 in_fname, be.bstrerror());
141 goto get_out;
142 }
143 wbytes = fwrite(buf, 1, rbytes, outfile);
144 Dmsg2(dbglvl+10, "Wrote: %d bytes wanted %d bytes.\n", wbytes, rbytes);
145 if (wbytes < 0) {
146 berrno be;
147 Mmsg2(xfer->m_message, "Error writing output file %s. ERR=%s\n",
148 out_fname, be.bstrerror());
149 goto get_out;
150 }
151 obj_len -= rbytes;
152 xfer->increment_processed_size(rbytes);
153 if (limit->use_bwlimit()) {
154 limit->control_bwlimit(rbytes);
155 }
156 }
157
158 get_out:
159 free_memory(buf);
160 if (infile) {
161 fclose(infile);
162 }
163 if (outfile) {
164 if (fclose(outfile) != 0) {
165 berrno be;
166 Mmsg2(xfer->m_message, "Failed to close file %s: %s\n", out_fname, be.bstrerror());
167 }
168 /* Get stats on the result part and fill the xfer res */
169 if (lstat(out_fname, &statbuf) == -1) {
170 berrno be;
171 Mmsg2(xfer->m_message, "Failed to stat file %s: %s\n", out_fname, be.bstrerror());
172 } else {
173 xfer->m_res_size = statbuf.st_size;
174 xfer->m_res_mtime = statbuf.st_mtime;
175 }
176 }
177 Leave(dbglvl);
178 return (xfer->m_message[0] == 0);
179 }
180
get_cloud_object(transfer * xfer,const char * cloud_fname,const char * cache_fname)181 bool file_driver::get_cloud_object(transfer *xfer, const char *cloud_fname, const char *cache_fname)
182 {
183 return put_object(xfer, cloud_fname, cache_fname, &download_limit);
184 }
185
truncate_cloud_volume(const char * VolumeName,ilist * trunc_parts,cancel_callback * cancel_cb,POOLMEM * & err)186 bool file_driver::truncate_cloud_volume(const char *VolumeName, ilist *trunc_parts, cancel_callback *cancel_cb, POOLMEM *&err)
187 {
188 bool rtn = true;
189 int i;
190 POOLMEM *filename = get_pool_memory(PM_FNAME);
191 for (i=1; (i <= (int)trunc_parts->last_index()); i++) {
192 if (!trunc_parts->get(i)) {
193 continue;
194 }
195 make_cloud_filename(filename, VolumeName, "part", i);
196 if (unlink(filename) != 0 && errno != ENOENT) {
197 berrno be;
198 Mmsg3(err, "truncate_cloud_volume for %s: Unable to delete %s. ERR=%s\n", VolumeName, filename, be.bstrerror());
199 rtn = false;
200 } else {
201 Mmsg2(err, "truncate_cloud_volume for %s: Unlink file %s.\n", VolumeName, filename);
202 }
203 }
204
205 free_pool_memory(filename);
206 return rtn;
207 }
208
clean_cloud_volume(const char * VolumeName,cleanup_cb_type * cb,cleanup_ctx_type * ctx,cancel_callback * cancel_cb,POOLMEM * & err)209 bool file_driver::clean_cloud_volume(const char *VolumeName, cleanup_cb_type *cb, cleanup_ctx_type *ctx, cancel_callback *cancel_cb, POOLMEM *&err)
210 {
211 Enter(dbglvl);
212
213 if (cb == NULL || ctx == NULL || strlen(VolumeName) == 0) {
214 pm_strcpy(err, "Invalid argument");
215 return false;
216 }
217
218 POOLMEM *vol_dir = get_pool_memory(PM_NAME);
219
220 pm_strcpy(vol_dir, hostName);
221
222 if (!IsPathSeparator(vol_dir[strlen(vol_dir)-1])) {
223 pm_strcat(vol_dir, "/");
224 }
225 pm_strcat(vol_dir, VolumeName);
226
227 DIR* dp = NULL;
228 struct dirent *entry = NULL;
229 struct stat statbuf;
230 int name_max;
231 bool ok = false;
232 POOL_MEM dname(PM_FNAME);
233 int status = 0;
234
235 Dmsg1(dbglvl, "Searching for parts in: %s\n", vol_dir);
236
237 if (!(dp = opendir(vol_dir))) {
238 berrno be;
239 Mmsg2(err, "Cannot opendir to get parts list. Volume %s does not exist. ERR=%s",
240 VolumeName, be.bstrerror());
241 Dmsg1(dbglvl, "%s\n", err);
242 if (errno == ENOENT) {
243 ok=true; /* No volume, so no part */
244 }
245 goto get_out;
246 }
247
248 name_max = pathconf(".", _PC_NAME_MAX);
249 if (name_max < 1024) {
250 name_max = 1024;
251 }
252
253 entry = (struct dirent *)malloc(sizeof(struct dirent) + name_max + 1000);
254
255 for ( ;; ) {
256 if (cancel_cb && cancel_cb->fct && cancel_cb->fct(cancel_cb->arg)) {
257 pm_strcpy(err, "Job canceled");
258 goto get_out;
259 }
260 errno = 0;
261 status = breaddir(dp, dname.addr());
262 if (status != 0) {
263 if (status > 0) {
264 Mmsg1(err, "breaddir failed: status=%d", status);
265 Dmsg1(dbglvl, "%s\n", err);
266 }
267 break;
268 }
269 /* Always ignore . and .. */
270 if (strcmp(".", dname.c_str()) == 0 || strcmp("..", dname.c_str()) == 0) {
271 continue;
272 }
273
274 POOLMEM *part_path = get_pool_memory(PM_NAME);
275 pm_strcpy(part_path,vol_dir);
276 if (!IsPathSeparator(part_path[strlen(vol_dir)-1])) {
277 pm_strcat(part_path, "/");
278 }
279 pm_strcat(part_path, dname.c_str());
280
281 /* Get size of part */
282 if (lstat(part_path, &statbuf) == -1) {
283 berrno be;
284 Mmsg(err, "Failed to stat file %s: %s", part_path, be.bstrerror());
285 Dmsg1(dbglvl, "%s\n", err);
286 free_pool_memory(part_path);
287 goto get_out;
288 }
289
290 POOLMEM *clean_part_path = get_pool_memory(PM_NAME);
291 pm_strcpy(clean_part_path,VolumeName);
292 if (!IsPathSeparator(clean_part_path[strlen(VolumeName)-1])) {
293 pm_strcat(clean_part_path, "/");
294 }
295 pm_strcat(clean_part_path, dname.c_str());
296 /* Look only for part files */
297 if (!cb(clean_part_path, ctx))
298 {
299 free_pool_memory(clean_part_path);
300 free_pool_memory(part_path);
301 continue;
302 }
303
304 free_pool_memory(clean_part_path);
305
306 if (unlink(part_path) != 0 && errno != ENOENT) {
307 berrno be;
308 Mmsg3(err, "truncate_cloud_volume for %s: Unable to delete %s. ERR=%s\n", VolumeName, part_path, be.bstrerror());
309 free_pool_memory(part_path);
310 goto get_out;
311 } else {
312 Dmsg2(dbglvl, "clean_cloud_volume for %s: Unlink file %s.\n", VolumeName, part_path);
313 }
314
315 free_pool_memory(part_path);
316 }
317 ok = true;
318
319 get_out:
320 if (dp) {
321 closedir(dp);
322 }
323 if (entry) {
324 free(entry);
325 }
326
327 free_pool_memory(vol_dir);
328
329 return ok;
330 }
331
make_cloud_filename(POOLMEM * & filename,const char * VolumeName,const char * file,uint32_t part)332 void file_driver::make_cloud_filename(POOLMEM *&filename,
333 const char *VolumeName, const char *file, uint32_t part)
334 {
335 Enter(dbglvl);
336 pm_strcpy(filename, hostName);
337 cloud_driver::add_vol_and_part(filename, VolumeName, file, part);
338 Dmsg1(dbglvl, "make_cloud_filename: %s\n", filename);
339 }
340
make_cloud_filename(POOLMEM * & filename,const char * VolumeName,const char * file)341 void file_driver::make_cloud_filename(POOLMEM *&filename,
342 const char *VolumeName, const char *file)
343 {
344 Enter(dbglvl);
345 pm_strcpy(filename, hostName);
346 cloud_driver::add_vol_and_part(filename, VolumeName, file);
347 Dmsg1(dbglvl, "make_cloud_filename: %s\n", filename);
348 }
349
350 /*
351 * Copy a single cache part to the cloud (local disk)
352 */
copy_cache_part_to_cloud(transfer * xfer)353 bool file_driver::copy_cache_part_to_cloud(transfer *xfer)
354 {
355 Enter(dbglvl);
356 POOLMEM *cloud_fname = get_pool_memory(PM_FNAME);
357 make_cloud_filename(cloud_fname, xfer->m_volume_name, "part", xfer->m_part);
358 Dmsg2(dbglvl, "Call put_object: %s, %s\n", xfer->m_cache_fname, cloud_fname);
359 bool rtn = put_object(xfer, xfer->m_cache_fname, cloud_fname, &upload_limit);
360 free_pool_memory(cloud_fname);
361 return rtn;
362 }
363
move_cloud_part(const char * VolumeName,uint32_t apart,const char * to,cancel_callback * cancel_cb,POOLMEM * & err,int & exists)364 bool file_driver::move_cloud_part(const char *VolumeName, uint32_t apart , const char *to, cancel_callback *cancel_cb, POOLMEM *&err, int& exists)
365 {
366 Enter(dbglvl);
367 bool rtn = false;
368 POOLMEM *cloud_source_name = get_pool_memory(PM_FNAME);
369 POOLMEM *cloud_dest_name = get_pool_memory(PM_FNAME);
370 make_cloud_filename(cloud_source_name, VolumeName, "part", apart);
371 make_cloud_filename(cloud_dest_name, VolumeName, to);
372 struct stat statbuf;
373 /* Get size of part */
374 if (lstat(cloud_source_name, &statbuf) != 0) {
375 exists = 0;
376 rtn = true;
377 } else {
378 exists = 1;
379 transfer xfer(statbuf.st_size, NULL, cloud_source_name, VolumeName, apart, NULL, NULL, NULL);
380 rtn = put_object(&xfer, cloud_source_name, cloud_dest_name, &upload_limit);
381 Mmsg(err,"%s",rtn ? to:xfer.m_message);
382 }
383 free_pool_memory(cloud_dest_name);
384 free_pool_memory(cloud_source_name);
385 return rtn;
386 }
387
388 /*
389 * Copy a single object (part) from the cloud to the cache
390 */
copy_cloud_part_to_cache(transfer * xfer)391 int file_driver::copy_cloud_part_to_cache(transfer *xfer)
392 {
393 Enter(dbglvl);
394
395 POOL_MEM cloud_fname(PM_FNAME);
396 make_cloud_filename(cloud_fname.addr(), xfer->m_volume_name, "part", xfer->m_part);
397
398 if (getenv("CLOUD_FILE_DRIVER_SIMULATE_DELAYED_TRANSFER") && xfer->m_debug_retry) {
399 restore_cloud_object(xfer, cloud_fname.c_str());
400 return CLOUD_DRIVER_COPY_PART_TO_CACHE_RETRY;
401 } else {
402 int ret = put_object(xfer, cloud_fname.c_str(), xfer->m_cache_fname, &download_limit);
403 if (ret) {
404 return CLOUD_DRIVER_COPY_PART_TO_CACHE_OK;
405 } else {
406 return CLOUD_DRIVER_COPY_PART_TO_CACHE_ERROR;
407 }
408 }
409 return CLOUD_DRIVER_COPY_PART_TO_CACHE_OK;
410 }
411
restore_cloud_object(transfer * xfer,const char * cloud_fname)412 bool file_driver::restore_cloud_object(transfer *xfer, const char *cloud_fname)
413 {
414 wait_timeout = time(NULL) + atoi(getenv("CLOUD_FILE_DRIVER_SIMULATE_DELAYED_TRANSFER"));
415 /* retry only once for DELAYED_TRANSFER time */
416 xfer->m_debug_retry = false;
417 return true;
418 }
419
is_waiting_on_server(transfer * xfer)420 bool file_driver::is_waiting_on_server(transfer *xfer)
421 {
422 (void) (xfer);
423 return (time(NULL)<wait_timeout);
424 }
425 /*
426 * NOTE: The SD Cloud resource has the following items
427
428 RES hdr;
429 char *host_name;
430 char *bucket_name;
431 char *access_key;
432 char *secret_key;
433 int32_t protocol;
434 int32_t uri_style;
435 uint32_t driver_type;
436 uint32_t trunc_opt;
437 uint32_t upload_opt;
438 */
439
init(CLOUD * cloud,POOLMEM * & err)440 bool file_driver::init(CLOUD *cloud, POOLMEM *&err)
441 {
442 if (cloud->host_name == NULL) {
443 Mmsg1(err, "Failed to initialize File Cloud. ERR=Hostname not set in cloud resource %s\n", cloud->hdr.name);
444 return false;
445 }
446 /* File I/O buffer */
447 buf_len = DEFAULT_BLOCK_SIZE;
448
449 hostName = cloud->host_name;
450 bucketName = cloud->bucket_name;
451 protocol = cloud->protocol;
452 uriStyle = cloud->uri_style;
453 accessKeyId = cloud->access_key;
454 secretAccessKey = cloud->secret_key;
455
456 return true;
457 }
458
start_of_job(POOLMEM * & msg)459 bool file_driver::start_of_job(POOLMEM *&msg)
460 {
461 Mmsg(msg, _("Using File cloud driver Host=%s Bucket=%s"), hostName, bucketName);
462 return true;
463 }
464
end_of_job(POOLMEM * & msg)465 bool file_driver::end_of_job(POOLMEM *&msg)
466 {
467 return true;
468 }
469
term(POOLMEM * & msg)470 bool file_driver::term(POOLMEM *&msg)
471 {
472 return true;
473 }
474
get_cloud_volume_parts_list(const char * VolumeName,ilist * parts,cancel_callback * cancel_cb,POOLMEM * & err)475 bool file_driver::get_cloud_volume_parts_list(const char* VolumeName, ilist *parts, cancel_callback *cancel_cb, POOLMEM *&err)
476 {
477 Enter(dbglvl);
478
479 if (parts == NULL || strlen(VolumeName) == 0) {
480 pm_strcpy(err, "Invalid argument");
481 return false;
482 }
483
484 POOLMEM *vol_dir = get_pool_memory(PM_NAME);
485
486 pm_strcpy(vol_dir, hostName);
487
488 if (!IsPathSeparator(vol_dir[strlen(vol_dir)-1])) {
489 pm_strcat(vol_dir, "/");
490 }
491 pm_strcat(vol_dir, VolumeName);
492
493 DIR* dp = NULL;
494 struct dirent *entry = NULL;
495 struct stat statbuf;
496 int name_max;
497 bool ok = false;
498 POOL_MEM dname(PM_FNAME);
499 int status = 0;
500
501 Dmsg1(dbglvl, "Searching for parts in: %s\n", vol_dir);
502
503 if (!(dp = opendir(vol_dir))) {
504 berrno be;
505 Mmsg2(err, "Cannot opendir to get parts list. Volume %s does not exist. ERR=%s",
506 VolumeName, be.bstrerror());
507 Dmsg1(dbglvl, "%s\n", err);
508 if (errno == ENOENT) {
509 ok=true; /* No volume, so no part */
510 }
511 goto get_out;
512 }
513
514 name_max = pathconf(".", _PC_NAME_MAX);
515 if (name_max < 1024) {
516 name_max = 1024;
517 }
518
519 entry = (struct dirent *)malloc(sizeof(struct dirent) + name_max + 1000);
520
521 for ( ;; ) {
522 if (cancel_cb && cancel_cb->fct && cancel_cb->fct(cancel_cb->arg)) {
523 pm_strcpy(err, "Job canceled");
524 goto get_out;
525 }
526 errno = 0;
527 status = breaddir(dp, dname.addr());
528 if (status != 0) {
529 if (status > 0) {
530 Mmsg1(err, "breaddir failed: status=%d", status);
531 Dmsg1(dbglvl, "%s\n", err);
532 }
533 break;
534 }
535 /* Always ignore . and .. */
536 if (strcmp(".", dname.c_str()) == 0 || strcmp("..", dname.c_str()) == 0) {
537 continue;
538 }
539
540 /* Look only for part files */
541 if (strncmp("part.", dname.c_str(), 5) != 0) {
542 continue;
543 }
544
545 char *ext = strrchr (dname.c_str(), '.');
546 if (!ext || strlen(ext) < 2) {
547 continue;
548 }
549
550 cloud_part *part = (cloud_part*) malloc(sizeof(cloud_part));
551
552 /* save extension (part number) to cloud_part struct index*/
553 part->index = atoi(&ext[1]);
554
555 POOLMEM *part_path = get_pool_memory(PM_NAME);
556 pm_strcpy(part_path,vol_dir);
557 if (!IsPathSeparator(part_path[strlen(vol_dir)-1])) {
558 pm_strcat(part_path, "/");
559 }
560 pm_strcat(part_path, dname.c_str());
561
562 /* Get size of part */
563 if (lstat(part_path, &statbuf) == -1) {
564 berrno be;
565 Mmsg(err, "Failed to stat file %s: %s", part_path, be.bstrerror());
566 Dmsg1(dbglvl, "%s\n", err);
567 free_pool_memory(part_path);
568 free(part);
569 goto get_out;
570 }
571 free_pool_memory(part_path);
572
573 part->size = statbuf.st_size;
574 part->mtime = statbuf.st_mtime;
575 bmemzero(part->hash64, 64);
576
577 parts->put(part->index, part);
578 }
579 ok = true;
580
581 get_out:
582 if (dp) {
583 closedir(dp);
584 }
585 if (entry) {
586 free(entry);
587 }
588
589 free_pool_memory(vol_dir);
590
591 return ok;
592 }
593
get_cloud_volumes_list(alist * volumes,cancel_callback * cancel_cb,POOLMEM * & err)594 bool file_driver::get_cloud_volumes_list(alist *volumes, cancel_callback *cancel_cb, POOLMEM *&err)
595 {
596 if (!volumes) {
597 pm_strcpy(err, "Invalid argument");
598 return false;
599 }
600
601 Enter(dbglvl);
602
603 DIR* dp = NULL;
604 struct dirent *entry = NULL;
605 struct stat statbuf;
606 int name_max;
607 bool ok = false;
608 POOLMEM *fullpath = get_pool_memory(PM_NAME);
609 POOL_MEM dname(PM_FNAME);
610 int status = 0;
611
612 if (!(dp = opendir(hostName))) {
613 berrno be;
614 Mmsg2(err, "Cannot opendir to get volumes list. host_name %s does not exist. ERR=%s",
615 hostName, be.bstrerror());
616 Dmsg1(dbglvl, "%s\n", err);
617 if (errno == ENOENT) {
618 ok=true; /* No volume, so no part */
619 }
620 goto get_out;
621 }
622
623 name_max = pathconf(".", _PC_NAME_MAX);
624 if (name_max < 1024) {
625 name_max = 1024;
626 }
627
628 entry = (struct dirent *)malloc(sizeof(struct dirent) + name_max + 1000);
629
630 for ( ;; ) {
631 if (cancel_cb && cancel_cb->fct && cancel_cb->fct(cancel_cb->arg)) {
632 goto get_out;
633 }
634 errno = 0;
635 status = breaddir(dp, dname.addr());
636 if (status != 0) {
637 if (status > 0) {
638 Mmsg1(err, "breaddir failed: status=%d", status);
639 Dmsg1(dbglvl, "%s\n", err);
640 }
641 break;
642 }
643 /* Always ignore . and .. */
644 if (strcmp(".", dname.c_str()) == 0 || strcmp("..", dname.c_str()) == 0) {
645 continue;
646 }
647
648
649 pm_strcpy(fullpath, hostName);
650 if (!IsPathSeparator(fullpath[strlen(fullpath)-1])) {
651 pm_strcat(fullpath, "/");
652 }
653 pm_strcat(fullpath, dname.c_str());
654
655 if (lstat(fullpath, &statbuf) != 0) {
656 berrno be;
657 Dmsg2(dbglvl, "Failed to stat file %s: %s\n",
658 fullpath, be.bstrerror());
659 continue;
660 }
661
662 if (S_ISDIR(statbuf.st_mode)) {
663 volumes->append(bstrdup(dname.c_str()));
664 }
665 }
666 ok = true;
667
668 get_out:
669 if (dp) {
670 closedir(dp);
671 }
672 if (entry) {
673 free(entry);
674 }
675
676 free_pool_memory(fullpath);
677
678 return ok;
679 }
680