3 * Bacula Director -- User Agent Database restore Command
4 * Creates a bootstrap file for restoring files and
5 * starts the restore job.
7 * Tree handling routines split into ua_tree.c July MMIII.
8 * BSR (bootstrap record) handling routines split into
11 * Kern Sibbald, July MMII
17 Copyright (C) 2002-2003 Kern Sibbald and John Walker
19 This program is free software; you can redistribute it and/or
20 modify it under the terms of the GNU General Public License as
21 published by the Free Software Foundation; either version 2 of
22 the License, or (at your option) any later version.
24 This program is distributed in the hope that it will be useful,
25 but WITHOUT ANY WARRANTY; without even the implied warranty of
26 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27 General Public License for more details.
29 You should have received a copy of the GNU General Public
30 License along with this program; if not, write to the Free
31 Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
40 /* Imported functions */
41 extern int runcmd(UAContext *ua, char *cmd);
43 /* Imported variables */
44 extern char *uar_list_jobs, *uar_file, *uar_sel_files;
45 extern char *uar_del_temp, *uar_del_temp1, *uar_create_temp;
46 extern char *uar_create_temp1, *uar_last_full, *uar_full;
47 extern char *uar_inc_dec, *uar_list_temp, *uar_sel_jobid_temp;
48 extern char *uar_sel_all_temp1, *uar_sel_fileset, *uar_mediatype;
49 extern char *uar_jobid_fileindex;
53 char **name; /* list of names */
54 int num_ids; /* ids stored */
55 int max_ids; /* size of array */
56 int num_del; /* number deleted */
57 int tot_ids; /* total to process */
61 /* Main structure for obtaining JobIds or Files to be restored */
66 char ClientName[MAX_NAME_LENGTH];
68 POOLMEM *JobIds; /* User entered string of JobIds */
72 uint32_t selected_files;
85 #define MAX_ID_LIST_LEN 1000000
88 /* Forward referenced functions */
89 static int last_full_handler(void *ctx, int num_fields, char **row);
90 static int jobid_handler(void *ctx, int num_fields, char **row);
91 static int next_jobid_from_list(char **p, uint32_t *JobId);
92 static int user_select_jobids_or_files(UAContext *ua, RESTORE_CTX *rx);
93 static int fileset_handler(void *ctx, int num_fields, char **row);
94 static void print_name_list(UAContext *ua, NAME_LIST *name_list);
95 static int unique_name_list_handler(void *ctx, int num_fields, char **row);
96 static void free_name_list(NAME_LIST *name_list);
97 static void get_storage_from_mediatype(UAContext *ua, NAME_LIST *name_list, RESTORE_CTX *rx);
98 static int select_backups_before_date(UAContext *ua, RESTORE_CTX *rx, char *date);
99 static void build_directory_tree(UAContext *ua, RESTORE_CTX *rx);
100 static void free_rx(RESTORE_CTX *rx);
101 static void split_path_and_filename(RESTORE_CTX *rx, char *fname);
102 static int jobid_fileindex_handler(void *ctx, int num_fields, char **row);
103 static int insert_file_into_findex_list(UAContext *ua, RESTORE_CTX *rx, char *file);
104 static void insert_one_file(UAContext *ua, RESTORE_CTX *rx);
105 static int get_client_name(UAContext *ua, RESTORE_CTX *rx);
111 int restorecmd(UAContext *ua, char *cmd)
113 RESTORE_CTX rx; /* restore context */
117 memset(&rx, 0, sizeof(rx));
119 rx.path = get_pool_memory(PM_FNAME);
120 rx.fname = get_pool_memory(PM_FNAME);
121 rx.JobIds = get_pool_memory(PM_FNAME);
122 rx.query = get_pool_memory(PM_FNAME);
125 i = find_arg_with_value(ua, "where");
127 rx.where = ua->argv[i];
135 /* Ensure there is at least one Restore Job */
137 while ( (job = (JOB *)GetNextRes(R_JOB, (RES *)job)) ) {
138 if (job->JobType == JT_RESTORE) {
139 if (!rx.restore_job) {
140 rx.restore_job = job;
146 if (!rx.restore_jobs) {
148 "No Restore Job Resource found. You must create at least\n"
149 "one before running this command.\n"));
155 * Request user to select JobIds or files by various different methods
156 * last 20 jobs, where File saved, most recent backup, ...
157 * In the end, a list of files are pumped into
160 switch (user_select_jobids_or_files(ua, &rx)) {
163 return 0; /* error */
164 case 1: /* select by jobid */
165 build_directory_tree(ua, &rx);
172 if (!complete_bsr(ua, rx.bsr)) { /* find Vol, SessId, SessTime from JobIds */
173 bsendmsg(ua, _("Unable to construct a valid BSR. Cannot continue.\n"));
177 // print_bsr(ua, rx.bsr);
178 write_bsr_file(ua, rx.bsr);
179 bsendmsg(ua, _("\n%u file%s selected to restore.\n\n"), rx.selected_files,
180 rx.selected_files==1?"":"s");
182 bsendmsg(ua, _("No files selected to restore.\n"));
187 if (rx.restore_jobs == 1) {
188 job = rx.restore_job;
190 job = select_restore_job_resource(ua);
193 bsendmsg(ua, _("No Restore Job resource found!\n"));
198 get_client_name(ua, &rx);
200 /* Build run command */
203 "run job=\"%s\" client=\"%s\" storage=\"%s\" bootstrap=\"%s/restore.bsr\""
205 job->hdr.name, rx.ClientName, rx.store?rx.store->hdr.name:"",
206 working_directory, rx.where);
209 "run job=\"%s\" client=\"%s\" storage=\"%s\" bootstrap=\"%s/restore.bsr\"",
210 job->hdr.name, rx.ClientName, rx.store?rx.store->hdr.name:"",
214 Dmsg1(400, "Submitting: %s\n", ua->cmd);
218 bsendmsg(ua, _("Restore command done.\n"));
223 static void free_rx(RESTORE_CTX *rx)
227 free_pool_memory(rx->JobIds);
231 free_pool_memory(rx->fname);
235 free_pool_memory(rx->path);
239 free_pool_memory(rx->query);
242 free_name_list(&rx->name_list);
245 static int get_client_name(UAContext *ua, RESTORE_CTX *rx)
247 /* If no client name specified yet, get it now */
248 if (!rx->ClientName[0]) {
250 /* try command line argument */
251 int i = find_arg_with_value(ua, _("client"));
253 bstrncpy(rx->ClientName, ua->argv[i], sizeof(rx->ClientName));
256 memset(&cr, 0, sizeof(cr));
257 if (!get_client_dbr(ua, &cr)) {
261 bstrncpy(rx->ClientName, cr.Name, sizeof(rx->ClientName));
267 * The first step in the restore process is for the user to
268 * select a list of JobIds from which he will subsequently
269 * select which files are to be restored.
271 static int user_select_jobids_or_files(UAContext *ua, RESTORE_CTX *rx)
274 char date[MAX_TIME_LENGTH];
280 "List last 20 Jobs run",
281 "List Jobs where a given File is saved",
282 "Enter list of JobIds to select",
283 "Enter SQL list command",
284 "Select the most recent backup for a client",
285 "Select backup for a client before a specified time",
286 "Enter a list of files to restore",
298 switch (find_arg_keyword(ua, kw)) {
300 i = find_arg_with_value(ua, _("jobid"));
304 pm_strcpy(&rx->JobIds, ua->argv[i]);
307 case 1: /* current */
308 bstrutime(date, sizeof(date), time(NULL));
309 if (!select_backups_before_date(ua, rx, date)) {
315 i = find_arg_with_value(ua, _("before"));
319 if (str_to_utime(ua->argv[i]) == 0) {
320 bsendmsg(ua, _("Improper date format: %s\n"), ua->argv[i]);
323 bstrncpy(date, ua->argv[i], sizeof(date));
324 if (!select_backups_before_date(ua, rx, date)) {
330 if (!get_client_name(ua, rx)) {
334 i = find_arg_with_value(ua, _("file"));
338 pm_strcpy(&ua->cmd, ua->argv[i]);
339 insert_one_file(ua, rx);
342 /* Check MediaType and select storage that corresponds */
343 get_storage_from_mediatype(ua, &rx->name_list, rx);
350 bsendmsg(ua, _("\nFirst you select one or more JobIds that contain files\n"
351 "to be restored. You will be presented several methods\n"
352 "of specifying the JobIds. Then you will be allowed to\n"
353 "select which files from those JobIds are to be restored.\n\n"));
356 /* If choice not already made above, prompt */
361 start_prompt(ua, _("To select the JobIds, you have the following choices:\n"));
362 for (int i=0; list[i]; i++) {
363 add_prompt(ua, list[i]);
366 switch (do_prompt(ua, "", _("Select item: "), NULL, 0)) {
369 case 0: /* list last 20 Jobs run */
370 db_list_sql_query(ua->jcr, ua->db, uar_list_jobs, prtit, ua, 1, HORZ_LIST);
373 case 1: /* list where a file is saved */
374 if (!get_cmd(ua, _("Enter Filename: "))) {
377 len = strlen(ua->cmd);
378 fname = (char *)malloc(len * 2 + 1);
379 db_escape_string(fname, ua->cmd, len);
380 Mmsg(&rx->query, uar_file, fname);
382 db_list_sql_query(ua->jcr, ua->db, rx->query, prtit, ua, 1, HORZ_LIST);
385 case 2: /* enter a list of JobIds */
386 if (!get_cmd(ua, _("Enter JobId(s), comma separated, to restore: "))) {
389 pm_strcpy(&rx->JobIds, ua->cmd);
391 case 3: /* Enter an SQL list command */
392 if (!get_cmd(ua, _("Enter SQL list command: "))) {
395 db_list_sql_query(ua->jcr, ua->db, ua->cmd, prtit, ua, 1, HORZ_LIST);
398 case 4: /* Select the most recent backups */
399 bstrutime(date, sizeof(date), time(NULL));
400 if (!select_backups_before_date(ua, rx, date)) {
404 case 5: /* select backup at specified time */
405 bsendmsg(ua, _("The restored files will the most current backup\n"
406 "BEFORE the date you specify below.\n\n"));
408 if (!get_cmd(ua, _("Enter date as YYYY-MM-DD HH:MM:SS :"))) {
411 if (str_to_utime(ua->cmd) != 0) {
414 bsendmsg(ua, _("Improper date format.\n"));
416 bstrncpy(date, ua->cmd, sizeof(date));
417 if (!select_backups_before_date(ua, rx, date)) {
421 case 6: /* Enter files */
422 if (!get_client_name(ua, rx)) {
426 if (!get_cmd(ua, _("Enter filename: "))) {
429 len = strlen(ua->cmd);
433 insert_one_file(ua, rx);
435 /* Check MediaType and select storage that corresponds */
436 get_storage_from_mediatype(ua, &rx->name_list, rx);
439 case 7: /* Cancel or quit */
444 if (*rx->JobIds == 0) {
445 bsendmsg(ua, _("No Jobs selected.\n"));
448 bsendmsg(ua, _("You have selected the following JobId%s: %s\n"),
449 strchr(rx->JobIds,',')?"s":"",rx->JobIds);
451 memset(&jr, 0, sizeof(JOB_DBR));
454 for (p=rx->JobIds; ; ) {
455 int stat = next_jobid_from_list(&p, &JobId);
457 bsendmsg(ua, _("Invalid JobId in list.\n"));
463 if (jr.JobId == JobId) {
464 continue; /* duplicate of last JobId */
467 if (!db_get_job_record(ua->jcr, ua->db, &jr)) {
468 bsendmsg(ua, _("Unable to get Job record. ERR=%s\n"), db_strerror(ua->db));
471 rx->TotalFiles += jr.JobFiles;
476 static void insert_one_file(UAContext *ua, RESTORE_CTX *rx)
484 if ((ffd = fopen(p, "r")) == NULL) {
485 bsendmsg(ua, _("Cannot open file %s: ERR=%s\n"),
489 while (fgets(file, sizeof(file), ffd)) {
490 insert_file_into_findex_list(ua, rx, file);
495 insert_file_into_findex_list(ua, rx, ua->cmd);
501 * For a given file (path+filename), split into path and file, then
502 * lookup the most recent backup in the catalog to get the JobId
503 * and FileIndex, then insert them into the findex list.
505 static int insert_file_into_findex_list(UAContext *ua, RESTORE_CTX *rx, char *file)
507 strip_trailing_junk(file);
508 split_path_and_filename(rx, file);
509 Mmsg(&rx->query, uar_jobid_fileindex, rx->path, rx->fname, rx->ClientName);
511 if (!db_sql_query(ua->db, rx->query, jobid_fileindex_handler, (void *)rx)) {
512 bsendmsg(ua, _("Query failed: %s. ERR=%s\n"),
513 rx->query, db_strerror(ua->db));
516 bsendmsg(ua, _("No database record found for: %s\n"), file);
519 rx->selected_files++;
521 * Find the FileSets for this JobId and add to the name_list
523 Mmsg(&rx->query, uar_mediatype, rx->JobId);
524 if (!db_sql_query(ua->db, rx->query, unique_name_list_handler, (void *)&rx->name_list)) {
525 bsendmsg(ua, "%s", db_strerror(ua->db));
531 static void split_path_and_filename(RESTORE_CTX *rx, char *name)
535 /* Find path without the filename.
536 * I.e. everything after the last / is a "filename".
537 * OK, maybe it is a directory name, but we treat it like
538 * a filename. If we don't find a / then the whole name
539 * must be a path name (e.g. c:).
541 for (p=f=name; *p; p++) {
543 f = p; /* set pos of last slash */
546 if (*f == '/') { /* did we find a slash? */
547 f++; /* yes, point to filename */
548 } else { /* no, whole thing must be path name */
552 /* If filename doesn't exist (i.e. root directory), we
553 * simply create a blank name consisting of a single
554 * space. This makes handling zero length filenames
559 rx->fname = check_pool_memory_size(rx->fname, rx->fnl+1);
560 memcpy(rx->fname, f, rx->fnl); /* copy filename */
561 rx->fname[rx->fnl] = 0;
563 rx->fname[0] = ' '; /* blank filename */
570 rx->path = check_pool_memory_size(rx->path, rx->pnl+1);
571 memcpy(rx->path, name, rx->pnl);
572 rx->path[rx->pnl] = 0;
579 Dmsg2(100, "sllit path=%s file=%s\n", rx->path, rx->fname);
582 static void build_directory_tree(UAContext *ua, RESTORE_CTX *rx)
585 JobId_t JobId, last_JobId;
589 memset(&tree, 0, sizeof(TREE_CTX));
591 * Build the directory tree containing JobIds user selected
593 tree.root = new_tree(rx->TotalFiles);
594 tree.root->fname = nofname;
598 * For display purposes, the same JobId, with different volumes may
599 * appear more than once, however, we only insert it once.
602 for (p=rx->JobIds; next_jobid_from_list(&p, &JobId) > 0; ) {
604 if (JobId == last_JobId) {
605 continue; /* eliminate duplicate JobIds */
608 bsendmsg(ua, _("Building directory tree for JobId %u ...\n"), JobId);
611 * Find files for this JobId and insert them in the tree
613 Mmsg(&rx->query, uar_sel_files, JobId);
614 if (!db_sql_query(ua->db, rx->query, insert_tree_handler, (void *)&tree)) {
615 bsendmsg(ua, "%s", db_strerror(ua->db));
618 * Find the FileSets for this JobId and add to the name_list
620 Mmsg(&rx->query, uar_mediatype, JobId);
621 if (!db_sql_query(ua->db, rx->query, unique_name_list_handler, (void *)&rx->name_list)) {
622 bsendmsg(ua, "%s", db_strerror(ua->db));
625 bsendmsg(ua, "%d Job%s inserted into the tree and marked for extraction.\n",
626 items, items==1?"":"s");
628 /* Check MediaType and select storage that corresponds */
629 get_storage_from_mediatype(ua, &rx->name_list, rx);
631 if (find_arg(ua, _("all")) < 0) {
632 /* Let the user select which files to restore */
633 user_select_files_from_tree(&tree);
637 * Walk down through the tree finding all files marked to be
638 * extracted making a bootstrap file.
640 for (TREE_NODE *node=first_tree_node(tree.root); node; node=next_tree_node(node)) {
641 Dmsg2(400, "FI=%d node=0x%x\n", node->FileIndex, node);
643 Dmsg2(400, "type=%d FI=%d\n", node->type, node->FileIndex);
644 add_findex(rx->bsr, node->JobId, node->FileIndex);
645 rx->selected_files++;
649 free_tree(tree.root); /* free the directory tree */
654 * This routine is used to get the current backup or a backup
655 * before the specified date.
657 static int select_backups_before_date(UAContext *ua, RESTORE_CTX *rx, char *date)
662 char fileset_name[MAX_NAME_LENGTH];
666 /* Create temp tables */
667 db_sql_query(ua->db, uar_del_temp, NULL, NULL);
668 db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
669 if (!db_sql_query(ua->db, uar_create_temp, NULL, NULL)) {
670 bsendmsg(ua, "%s\n", db_strerror(ua->db));
672 if (!db_sql_query(ua->db, uar_create_temp1, NULL, NULL)) {
673 bsendmsg(ua, "%s\n", db_strerror(ua->db));
676 * Select Client from the Catalog
678 memset(&cr, 0, sizeof(cr));
679 if (!get_client_dbr(ua, &cr)) {
682 bstrncpy(rx->ClientName, cr.Name, sizeof(rx->ClientName));
687 Mmsg(&rx->query, uar_sel_fileset, cr.ClientId, cr.ClientId);
688 start_prompt(ua, _("The defined FileSet resources are:\n"));
689 if (!db_sql_query(ua->db, rx->query, fileset_handler, (void *)ua)) {
690 bsendmsg(ua, "%s\n", db_strerror(ua->db));
692 if (do_prompt(ua, _("FileSet"), _("Select FileSet resource"),
693 fileset_name, sizeof(fileset_name)) < 0) {
696 fsr.FileSetId = atoi(fileset_name); /* Id is first part of name */
697 if (!db_get_fileset_record(ua->jcr, ua->db, &fsr)) {
698 bsendmsg(ua, _("Error getting FileSet record: %s\n"), db_strerror(ua->db));
699 bsendmsg(ua, _("This probably means you modified the FileSet.\n"
700 "Continuing anyway.\n"));
704 /* Find JobId of last Full backup for this client, fileset */
705 Mmsg(&rx->query, uar_last_full, cr.ClientId, cr.ClientId, date, fsr.FileSetId);
706 if (!db_sql_query(ua->db, rx->query, NULL, NULL)) {
707 bsendmsg(ua, "%s\n", db_strerror(ua->db));
711 /* Find all Volumes used by that JobId */
712 if (!db_sql_query(ua->db, uar_full, NULL, NULL)) {
713 bsendmsg(ua, "%s\n", db_strerror(ua->db));
716 /* Note, this is needed as I don't seem to get the callback
717 * from the call just above.
720 if (!db_sql_query(ua->db, uar_sel_all_temp1, last_full_handler, (void *)rx)) {
721 bsendmsg(ua, "%s\n", db_strerror(ua->db));
723 if (rx->JobTDate == 0) {
724 bsendmsg(ua, _("No Full backup before %s found.\n"), date);
728 /* Now find all Incremental/Decremental Jobs after Full save */
729 Mmsg(&rx->query, uar_inc_dec, edit_uint64(rx->JobTDate, ed1), date,
730 cr.ClientId, fsr.FileSetId);
731 if (!db_sql_query(ua->db, rx->query, NULL, NULL)) {
732 bsendmsg(ua, "%s\n", db_strerror(ua->db));
735 /* Get the JobIds from that list */
737 rx->last_jobid[0] = 0;
738 if (!db_sql_query(ua->db, uar_sel_jobid_temp, jobid_handler, (void *)rx)) {
739 bsendmsg(ua, "%s\n", db_strerror(ua->db));
742 if (rx->JobIds[0] != 0) {
743 /* Display a list of Jobs selected for this restore */
744 db_list_sql_query(ua->jcr, ua->db, uar_list_temp, prtit, ua, 1, HORZ_LIST);
746 bsendmsg(ua, _("No jobs found.\n"));
752 db_sql_query(ua->db, uar_del_temp, NULL, NULL);
753 db_sql_query(ua->db, uar_del_temp1, NULL, NULL);
757 /* Return next JobId from comma separated list */
758 static int next_jobid_from_list(char **p, uint32_t *JobId)
764 for (int i=0; i<(int)sizeof(jobid); i++) {
765 if (*q == ',' || *q == 0) {
772 if (jobid[0] == 0 || !is_a_number(jobid)) {
776 *JobId = strtoul(jobid, NULL, 10);
781 * Callback handler to get JobId and FileIndex for files
783 static int jobid_fileindex_handler(void *ctx, int num_fields, char **row)
785 RESTORE_CTX *rx = (RESTORE_CTX *)ctx;
786 rx->JobId = atoi(row[0]);
787 add_findex(rx->bsr, rx->JobId, atoi(row[1]));
793 * Callback handler make list of JobIds
795 static int jobid_handler(void *ctx, int num_fields, char **row)
797 RESTORE_CTX *rx = (RESTORE_CTX *)ctx;
799 if (strcmp(rx->last_jobid, row[0]) == 0) {
800 return 0; /* duplicate id */
802 bstrncpy(rx->last_jobid, row[0], sizeof(rx->last_jobid));
803 pm_strcat(&rx->JobIds, ",");
804 pm_strcat(&rx->JobIds, row[0]);
810 * Callback handler to pickup last Full backup JobTDate
812 static int last_full_handler(void *ctx, int num_fields, char **row)
814 RESTORE_CTX *rx = (RESTORE_CTX *)ctx;
816 rx->JobTDate = strtoll(row[1], NULL, 10);
822 * Callback handler build fileset prompt list
824 static int fileset_handler(void *ctx, int num_fields, char **row)
826 char prompt[MAX_NAME_LENGTH+200];
828 snprintf(prompt, sizeof(prompt), "%s %s %s", row[0], row[1], row[2]);
829 add_prompt((UAContext *)ctx, prompt);
834 * Called here with each name to be added to the list. The name is
835 * added to the list if it is not already in the list.
837 * Used to make unique list of FileSets and MediaTypes
839 static int unique_name_list_handler(void *ctx, int num_fields, char **row)
841 NAME_LIST *name = (NAME_LIST *)ctx;
843 if (name->num_ids == MAX_ID_LIST_LEN) {
846 if (name->num_ids == name->max_ids) {
847 if (name->max_ids == 0) {
848 name->max_ids = 1000;
849 name->name = (char **)bmalloc(sizeof(char *) * name->max_ids);
851 name->max_ids = (name->max_ids * 3) / 2;
852 name->name = (char **)brealloc(name->name, sizeof(char *) * name->max_ids);
855 for (int i=0; i<name->num_ids; i++) {
856 if (strcmp(name->name[i], row[0]) == 0) {
857 return 0; /* already in list, return */
860 /* Add new name to list */
861 name->name[name->num_ids++] = bstrdup(row[0]);
867 * Print names in the list
869 static void print_name_list(UAContext *ua, NAME_LIST *name_list)
871 for (int i=0; i < name_list->num_ids; i++) {
872 bsendmsg(ua, "%s\n", name_list->name[i]);
878 * Free names in the list
880 static void free_name_list(NAME_LIST *name_list)
882 for (int i=0; i < name_list->num_ids; i++) {
883 free(name_list->name[i]);
885 if (name_list->name) {
886 free(name_list->name);
888 name_list->max_ids = 0;
889 name_list->num_ids = 0;
892 static void get_storage_from_mediatype(UAContext *ua, NAME_LIST *name_list, RESTORE_CTX *rx)
894 char name[MAX_NAME_LENGTH];
897 if (name_list->num_ids > 1) {
898 bsendmsg(ua, _("Warning, the JobIds that you selected refer to more than one MediaType.\n"
899 "Restore is not possible. The MediaTypes used are:\n"));
900 print_name_list(ua, name_list);
901 rx->store = select_storage_resource(ua);
905 if (name_list->num_ids == 0) {
906 bsendmsg(ua, _("No MediaType found for your JobIds.\n"));
907 rx->store = select_storage_resource(ua);
911 start_prompt(ua, _("The defined Storage resources are:\n"));
913 while ((store = (STORE *)GetNextRes(R_STORAGE, (RES *)store))) {
914 if (strcmp(store->media_type, name_list->name[0]) == 0) {
915 add_prompt(ua, store->hdr.name);
919 do_prompt(ua, _("Storage"), _("Select Storage resource"), name, sizeof(name));
920 rx->store = (STORE *)GetResWithName(R_STORAGE, name);
922 bsendmsg(ua, _("\nWarning. Unable to find Storage resource for\n"
923 "MediaType %s, needed by the Jobs you selected.\n"
924 "You will be allowed to select a Storage device later.\n"),