]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_prune.c
68c747cfce5a1df74bbc988bc06bcf4aa478c40d
[bacula/bacula] / bacula / src / dird / ua_prune.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2002-2009 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 three of the GNU Affero 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 Affero 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 Kern Sibbald.
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 -- User Agent Database prune Command
31  *      Applies retention periods
32  *
33  *     Kern Sibbald, February MMII
34  *
35  */
36
37 #include "bacula.h"
38 #include "dird.h"
39
40 /* Imported functions */
41
42 /* Forward referenced functions */
43 static bool grow_del_list(struct del_ctx *del);
44
45 /*
46  * Called here to count entries to be deleted
47  */
48 int del_count_handler(void *ctx, int num_fields, char **row)
49 {
50    struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
51
52    if (row[0]) {
53       cnt->count = str_to_int64(row[0]);
54    } else {
55       cnt->count = 0;
56    }
57    return 0;
58 }
59
60
61 /*
62  * Called here to make in memory list of JobIds to be
63  *  deleted and the associated PurgedFiles flag.
64  *  The in memory list will then be transversed
65  *  to issue the SQL DELETE commands.  Note, the list
66  *  is allowed to get to MAX_DEL_LIST_LEN to limit the
67  *  maximum malloc'ed memory.
68  */
69 int job_delete_handler(void *ctx, int num_fields, char **row)
70 {
71    struct del_ctx *del = (struct del_ctx *)ctx;
72
73    if (!grow_del_list(del)) {
74       return 1;
75    }
76    del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
77    Dmsg2(60, "job_delete_handler row=%d val=%d\n", del->num_ids, del->JobId[del->num_ids]);
78    del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
79    return 0;
80 }
81
82 int file_delete_handler(void *ctx, int num_fields, char **row)
83 {
84    struct del_ctx *del = (struct del_ctx *)ctx;
85
86    if (!grow_del_list(del)) {
87       return 1;
88    }
89    del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
90 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
91    return 0;
92 }
93
94 /*
95  *   Prune records from database
96  *
97  *    prune files (from) client=xxx [pool=yyy]
98  *    prune jobs (from) client=xxx [pool=yyy]
99  *    prune volume=xxx
100  *    prune stats
101  */
102 int prunecmd(UAContext *ua, const char *cmd)
103 {
104    DIRRES *dir;
105    CLIENT *client;
106    POOL *pool;
107    POOL_DBR pr;
108    MEDIA_DBR mr;
109    utime_t retention;
110    int kw;
111
112    static const char *keywords[] = {
113       NT_("Files"),
114       NT_("Jobs"),
115       NT_("Volume"),
116       NT_("Stats"),
117       NULL};
118
119    if (!open_client_db(ua)) {
120       return false;
121    }
122
123    /* First search args */
124    kw = find_arg_keyword(ua, keywords);
125    if (kw < 0 || kw > 3) {
126       /* no args, so ask user */
127       kw = do_keyword_prompt(ua, _("Choose item to prune"), keywords);
128    }
129
130    switch (kw) {
131    case 0:  /* prune files */
132       if (!(client = get_client_resource(ua))) {
133          return false;
134       }
135       if (find_arg_with_value(ua, "pool") >= 0) {
136          pool = get_pool_resource(ua);
137       } else {
138          pool = NULL;
139       }
140       /* Pool File Retention takes precedence over client File Retention */
141       if (pool && pool->FileRetention > 0) {
142          if (!confirm_retention(ua, &pool->FileRetention, "File")) {
143             return false;
144          }
145       } else if (!confirm_retention(ua, &client->FileRetention, "File")) {
146          return false;
147       }
148       prune_files(ua, client, pool);
149       return true;
150    case 1:  /* prune jobs */
151       if (!(client = get_client_resource(ua))) {
152          return false;
153       }
154       if (find_arg_with_value(ua, "pool") >= 0) {
155          pool = get_pool_resource(ua);
156       } else {
157          pool = NULL;
158       }
159       /* Pool Job Retention takes precedence over client Job Retention */
160       if (pool && pool->JobRetention > 0) {
161          if (!confirm_retention(ua, &pool->JobRetention, "Job")) {
162             return false;
163          }
164       } else if (!confirm_retention(ua, &client->JobRetention, "Job")) {
165          return false;
166       }
167       /* ****FIXME**** allow user to select JobType */
168       prune_jobs(ua, client, pool, JT_BACKUP);
169       return 1;
170    case 2:  /* prune volume */
171       if (!select_pool_and_media_dbr(ua, &pr, &mr)) {
172          return false;
173       }
174       if (mr.Enabled == 2) {
175          ua->error_msg(_("Cannot prune Volume \"%s\" because it is archived.\n"),
176             mr.VolumeName);
177          return false;
178       }
179       if (!confirm_retention(ua, &mr.VolRetention, "Volume")) {
180          return false;
181       }
182       prune_volume(ua, &mr);
183       return true;
184    case 3:  /* prune stats */
185       dir = (DIRRES *)GetNextRes(R_DIRECTOR, NULL);
186       if (!dir->stats_retention) {
187          return false;
188       }
189       retention = dir->stats_retention;
190       if (!confirm_retention(ua, &retention, "Statistics")) {
191          return false;
192       }
193       prune_stats(ua, retention);
194       return true;
195    default:
196       break;
197    }
198
199    return true;
200 }
201
202 /* Prune Job stat records from the database. 
203  *
204  */
205 int prune_stats(UAContext *ua, utime_t retention)
206 {
207    char ed1[50];
208    POOL_MEM query(PM_MESSAGE);
209    utime_t now = (utime_t)time(NULL);
210
211    db_lock(ua->db);
212    Mmsg(query, "DELETE FROM JobHisto WHERE JobTDate < %s", 
213         edit_int64(now - retention, ed1));
214    db_sql_query(ua->db, query.c_str(), NULL, NULL);
215    db_unlock(ua->db);
216
217    ua->info_msg(_("Pruned Jobs from JobHisto catalog.\n"));
218
219    return true;
220 }
221
222 /* 
223  * Use pool and client specified by user to select jobs to prune 
224  * returns add_from string to add in FROM clause
225  *         add_where string to add in WHERE clause
226  */
227 bool prune_set_filter(UAContext *ua, CLIENT *client, POOL *pool, utime_t period,
228                       POOL_MEM *add_from, POOL_MEM *add_where)
229 {
230    utime_t now;
231    char ed1[50], ed2[MAX_ESCAPE_NAME_LENGTH]; 
232    POOL_MEM tmp(PM_MESSAGE);
233
234    now = (utime_t)time(NULL);
235    edit_int64(now - period, ed1);
236    Dmsg3(150, "now=%lld period=%lld JobTDate=%s\n", now, period, ed1);
237    Mmsg(tmp, " AND JobTDate < %s ", ed1);
238    pm_strcat(*add_where, tmp.c_str());
239
240    db_lock(ua->db);
241    if (client) { 
242       db_escape_string(ua->jcr, ua->db, ed2, 
243                        client->name(), strlen(client->name()));
244       Mmsg(tmp, " AND Client.Name = '%s' ", ed2);
245       pm_strcat(*add_where, tmp.c_str());
246       pm_strcat(*add_from, " JOIN Client USING (ClientId) ");
247    }
248
249    if (pool) { 
250       db_escape_string(ua->jcr, ua->db, ed2, 
251                        pool->name(), strlen(pool->name()));
252       Mmsg(tmp, " AND Pool.Name = '%s' ", ed2);
253       pm_strcat(*add_where, tmp.c_str());
254       /* Use ON() instead of USING for some old SQLite */
255       pm_strcat(*add_from, " JOIN Pool ON (Job.PoolId = Pool.PoolId) ");
256    }
257    Dmsg2(150, "f=%s w=%s\n", add_from->c_str(), add_where->c_str());
258    db_unlock(ua->db);
259    return true;
260 }
261
262 /*
263  * Prune File records from the database. For any Job which
264  * is older than the retention period, we unconditionally delete
265  * all File records for that Job.  This is simple enough that no
266  * temporary tables are needed. We simply make an in memory list of
267  * the JobIds meeting the prune conditions, then delete all File records
268  * pointing to each of those JobIds.
269  *
270  * This routine assumes you want the pruning to be done. All checking
271  *  must be done before calling this routine.
272  *
273  * Note: client or pool can possibly be NULL (not both).
274  */
275 int prune_files(UAContext *ua, CLIENT *client, POOL *pool)
276 {
277    struct del_ctx del;
278    struct s_count_ctx cnt;
279    POOL_MEM query(PM_MESSAGE);
280    POOL_MEM sql_where(PM_MESSAGE);
281    POOL_MEM sql_from(PM_MESSAGE);
282    utime_t period;
283    char ed1[50];
284    
285    memset(&del, 0, sizeof(del));
286
287    if (pool && pool->FileRetention > 0) {
288       period = pool->FileRetention;
289
290    } else if (client) {
291       period = client->FileRetention;
292
293    } else {                     /* should specify at least pool or client */
294       return false;
295    }
296
297    db_lock(ua->db);
298    /* Specify JobTDate and Pool.Name= and/or Client.Name= in the query */
299    if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
300       goto bail_out;
301    }
302
303 //   edit_utime(now-period, ed1, sizeof(ed1));
304 //   Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s secs.\n"), ed1);
305    Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Files.\n"));
306    /* Select Jobs -- for counting */ 
307    Mmsg(query, 
308         "SELECT COUNT(1) FROM Job %s WHERE PurgedFiles=0 %s", 
309         sql_from.c_str(), sql_where.c_str());
310    Dmsg1(050, "select sql=%s\n", query.c_str());
311    cnt.count = 0;
312    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
313       ua->error_msg("%s", db_strerror(ua->db));
314       Dmsg0(050, "Count failed\n");
315       goto bail_out;
316    }
317
318    if (cnt.count == 0) {
319       if (ua->verbose) {
320          ua->warning_msg(_("No Files found to prune.\n"));
321       }
322       goto bail_out;
323    }
324
325    if (cnt.count < MAX_DEL_LIST_LEN) {
326       del.max_ids = cnt.count + 1;
327    } else {
328       del.max_ids = MAX_DEL_LIST_LEN;
329    }
330    del.tot_ids = 0;
331
332    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
333
334    /* Now process same set but making a delete list */
335    Mmsg(query, "SELECT JobId FROM Job %s WHERE PurgedFiles=0 %s", 
336         sql_from.c_str(), sql_where.c_str());
337    Dmsg1(050, "select sql=%s\n", query.c_str());
338    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
339
340    purge_files_from_job_list(ua, del);
341
342    edit_uint64_with_commas(del.num_del, ed1);
343    ua->info_msg(_("Pruned Files from %s Jobs for client %s from catalog.\n"),
344       ed1, client->name());
345
346 bail_out:
347    db_unlock(ua->db);
348    if (del.JobId) {
349       free(del.JobId);
350    }
351    return 1;
352 }
353
354
355 static void drop_temp_tables(UAContext *ua)
356 {
357    int i;
358    for (i=0; drop_deltabs[i]; i++) {
359       db_sql_query(ua->db, drop_deltabs[i], NULL, (void *)NULL);
360    }
361 }
362
363 static bool create_temp_tables(UAContext *ua)
364 {
365    /* Create temp tables and indicies */
366    if (!db_sql_query(ua->db, create_deltabs[db_get_type_index(ua->db)], NULL, (void *)NULL)) {
367       ua->error_msg("%s", db_strerror(ua->db));
368       Dmsg0(050, "create DelTables table failed\n");
369       return false;
370    }
371    if (!db_sql_query(ua->db, create_delindex, NULL, (void *)NULL)) {
372        ua->error_msg("%s", db_strerror(ua->db));
373        Dmsg0(050, "create DelInx1 index failed\n");
374        return false;
375    }
376    return true;
377 }
378
379 static bool grow_del_list(struct del_ctx *del)
380 {
381    if (del->num_ids == MAX_DEL_LIST_LEN) {
382       return false;
383    }
384
385    if (del->num_ids == del->max_ids) {
386       del->max_ids = (del->max_ids * 3) / 2;
387       del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
388          del->max_ids);
389       del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
390    }
391    return true;
392 }
393
394 struct accurate_check_ctx {
395    DBId_t ClientId;                   /* Id of client */
396    DBId_t FileSetId;                  /* Id of FileSet */ 
397 };
398
399 /* row: Job.Name, FileSet, Client.Name, FileSetId, ClientId, Type */
400 static int job_select_handler(void *ctx, int num_fields, char **row)
401 {
402    alist *lst = (alist *)ctx;
403    struct accurate_check_ctx *res;
404    ASSERT(num_fields == 6);
405
406    /* If this job doesn't exist anymore in the configuration, delete it */
407    if (GetResWithName(R_JOB, row[0]) == NULL) {
408       return 0;
409    }
410
411    /* If this fileset doesn't exist anymore in the configuration, delete it */
412    if (GetResWithName(R_FILESET, row[1]) == NULL) {
413       return 0;
414    }
415
416    /* If this client doesn't exist anymore in the configuration, delete it */
417    if (GetResWithName(R_CLIENT, row[2]) == NULL) {
418       return 0;
419    }
420
421    /* Don't compute accurate things for Verify jobs */
422    if (*row[5] == 'V') {
423       return 0;
424    }
425
426    res = (struct accurate_check_ctx*) malloc(sizeof(struct accurate_check_ctx));
427    res->FileSetId = str_to_int64(row[3]);
428    res->ClientId = str_to_int64(row[4]);
429    lst->append(res);
430
431 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
432    return 0;
433 }
434
435 /*
436  * Pruning Jobs is a bit more complicated than purging Files
437  * because we delete Job records only if there is a more current
438  * backup of the FileSet. Otherwise, we keep the Job record.
439  * In other words, we never delete the only Job record that
440  * contains a current backup of a FileSet. This prevents the
441  * Volume from being recycled and destroying a current backup.
442  *
443  * For Verify Jobs, we do not delete the last InitCatalog.
444  *
445  * For Restore Jobs there are no restrictions.
446  */
447 int prune_jobs(UAContext *ua, CLIENT *client, POOL *pool, int JobType)
448 {
449    POOL_MEM query(PM_MESSAGE);
450    POOL_MEM sql_where(PM_MESSAGE);
451    POOL_MEM sql_from(PM_MESSAGE);
452    utime_t period;
453    char ed1[50];
454    alist *jobids_check=NULL;
455    struct accurate_check_ctx *elt;
456    db_list_ctx jobids, tempids;
457    JOB_DBR jr;
458    struct del_ctx del;
459    memset(&del, 0, sizeof(del));
460
461    if (pool && pool->JobRetention > 0) {
462       period = pool->JobRetention;
463
464    } else if (client) {
465       period = client->JobRetention;
466
467    } else {                     /* should specify at least pool or client */
468       return false;
469    }
470
471    db_lock(ua->db);
472    if (!prune_set_filter(ua, client, pool, period, &sql_from, &sql_where)) {
473       goto bail_out;
474    }
475
476    /* Drop any previous temporary tables still there */
477    drop_temp_tables(ua);
478
479    /* Create temp tables and indicies */
480    if (!create_temp_tables(ua)) {
481       goto bail_out;
482    }
483
484    edit_utime(period, ed1, sizeof(ed1));
485    Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
486
487    del.max_ids = 100;
488    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
489    del.PurgedFiles = (char *)malloc(del.max_ids);
490
491    /*
492     * Select all files that are older than the JobRetention period
493     *  and add them into the "DeletionCandidates" table.
494     */
495    Mmsg(query, 
496         "INSERT INTO DelCandidates "
497           "SELECT JobId,PurgedFiles,FileSetId,JobFiles,JobStatus "
498             "FROM Job %s "      /* JOIN Pool/Client */
499            "WHERE Type IN ('B', 'C', 'M', 'V',  'D', 'R', 'c', 'm', 'g') "
500              " %s ",            /* Pool/Client + JobTDate */
501         sql_from.c_str(), sql_where.c_str());
502
503    Dmsg1(050, "select sql=%s\n", query.c_str());
504    if (!db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL)) {
505       if (ua->verbose) {
506          ua->error_msg("%s", db_strerror(ua->db));
507       }
508       goto bail_out;
509    }
510
511    /* Now, for the selection, we discard some of them in order to be always
512     * able to restore files. (ie, last full, last diff, last incrs)
513     * Note: The DISTINCT could be more useful if we don't get FileSetId
514     */
515    jobids_check = New(alist(10, owned_by_alist));
516    Mmsg(query, 
517 "SELECT DISTINCT Job.Name, FileSet, Client.Name, Job.FileSetId, "
518                 "Job.ClientId, Job.Type "
519   "FROM DelCandidates "
520        "JOIN Job USING (JobId) "
521        "JOIN Client USING (ClientId) "
522        "JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) "
523  "WHERE Job.Type IN ('B') "               /* Look only Backup jobs */
524    "AND Job.JobStatus IN ('T', 'W') "     /* Look only useful jobs */
525       );
526
527    /* The job_select_handler will skip jobs or filesets that are no longer
528     * in the configuration file. Interesting ClientId/FileSetId will be
529     * added to jobids_check
530     */
531    if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
532       ua->error_msg("%s", db_strerror(ua->db));
533    }
534
535    /* For this selection, we exclude current jobs used for restore or
536     * accurate. This will prevent to prune the last full backup used for
537     * current backup & restore
538     */
539    memset(&jr, 0, sizeof(jr));
540    /* To find useful jobs, we do like an incremental */
541    jr.JobLevel = L_INCREMENTAL; 
542    foreach_alist(elt, jobids_check) {
543       jr.ClientId = elt->ClientId;   /* should be always the same */
544       jr.FileSetId = elt->FileSetId;
545       db_accurate_get_jobids(ua->jcr, ua->db, &jr, &tempids);
546       jobids.add(tempids);
547    }
548
549    /* Discard latest Verify level=InitCatalog job 
550     * TODO: can have multiple fileset
551     */
552    Mmsg(query, 
553         "SELECT JobId, JobTDate "
554           "FROM Job %s "                         /* JOIN Client/Pool */
555          "WHERE Type='V'    AND Level='V' "
556               " %s "                             /* Pool, JobTDate, Client */
557          "ORDER BY JobTDate DESC LIMIT 1", 
558         sql_from.c_str(), sql_where.c_str());
559
560    if (!db_sql_query(ua->db, query.c_str(), db_list_handler, &jobids)) {
561       ua->error_msg("%s", db_strerror(ua->db));
562    }
563
564    /* If we found jobs to exclude from the DelCandidates list, we should
565     * also remove BaseJobs that can be linked with them
566     */
567    if (jobids.count > 0) {
568       Dmsg1(60, "jobids to exclude before basejobs = %s\n", jobids.list);
569       /* We also need to exclude all basejobs used */
570       db_get_used_base_jobids(ua->jcr, ua->db, jobids.list, &jobids);
571
572       /* Removing useful jobs from the DelCandidates list */
573       Mmsg(query, "DELETE FROM DelCandidates "
574                    "WHERE JobId IN (%s) "        /* JobId used in accurate */
575                      "AND JobFiles!=0",          /* Discard when JobFiles=0 */
576            jobids.list);
577
578       if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
579          ua->error_msg("%s", db_strerror(ua->db));
580          goto bail_out;         /* Don't continue if the list isn't clean */
581       }
582       Dmsg1(60, "jobids to exclude = %s\n", jobids.list);
583    }
584
585    /* We use DISTINCT because we can have two times the same job */
586    Mmsg(query, 
587         "SELECT DISTINCT DelCandidates.JobId,DelCandidates.PurgedFiles "
588           "FROM DelCandidates");
589    if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
590       ua->error_msg("%s", db_strerror(ua->db));
591    }
592
593    purge_job_list_from_catalog(ua, del);
594
595    if (del.num_del > 0) {
596       ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
597          del.num_del==1?_("Job"):_("Jobs"), client->name());
598     } else if (ua->verbose) {
599        ua->info_msg(_("No Jobs found to prune.\n"));
600     }
601
602 bail_out:
603    drop_temp_tables(ua);
604    db_unlock(ua->db);
605    if (del.JobId) {
606       free(del.JobId);
607    }
608    if (del.PurgedFiles) {
609       free(del.PurgedFiles);
610    }
611    if (jobids_check) {
612       delete jobids_check;
613    }
614    return 1;
615 }
616
617 /*
618  * Prune a given Volume
619  */
620 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
621 {
622    POOL_MEM query(PM_MESSAGE);
623    struct del_ctx del;
624    bool ok = false;
625    int count;
626
627    if (mr->Enabled == 2) {
628       return false;                   /* Cannot prune archived volumes */
629    }
630
631    memset(&del, 0, sizeof(del));
632    del.max_ids = 10000;
633    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
634
635    db_lock(ua->db);
636
637    /* Prune only Volumes with status "Full", or "Used" */
638    if (strcmp(mr->VolStatus, "Full")   == 0 ||
639        strcmp(mr->VolStatus, "Used")   == 0) {
640       Dmsg2(050, "get prune list MediaId=%d Volume %s\n", (int)mr->MediaId, mr->VolumeName);
641       count = get_prune_list_for_volume(ua, mr, &del);
642       Dmsg1(050, "Num pruned = %d\n", count);
643       if (count != 0) {
644          purge_job_list_from_catalog(ua, del);
645       }
646       ok = is_volume_purged(ua, mr);
647    }
648
649    db_unlock(ua->db);
650    if (del.JobId) {
651       free(del.JobId);
652    }
653    return ok;
654 }
655
656 /*
657  * Get prune list for a volume
658  */
659 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
660 {
661    POOL_MEM query(PM_MESSAGE);
662    int count = 0;
663    utime_t now, period;
664    char ed1[50], ed2[50];
665
666    if (mr->Enabled == 2) {
667       return 0;                    /* cannot prune Archived volumes */
668    }
669
670    /*
671     * Now add to the  list of JobIds for Jobs written to this Volume
672     */
673    edit_int64(mr->MediaId, ed1); 
674    period = mr->VolRetention;
675    now = (utime_t)time(NULL);
676    edit_int64(now-period, ed2);
677    Mmsg(query, sel_JobMedia, ed1, ed2);
678    Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
679       ed2);
680
681    Dmsg1(050, "Query=%s\n", query.c_str());
682    if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
683       if (ua->verbose) {
684          ua->error_msg("%s", db_strerror(ua->db));
685       }
686       Dmsg0(050, "Count failed\n");
687       goto bail_out;
688    }
689    count = exclude_running_jobs_from_list(del);
690    
691 bail_out:
692    return count;
693 }
694
695 /*
696  * We have a list of jobs to prune or purge. If any of them is
697  *   currently running, we set its JobId to zero which effectively
698  *   excludes it.
699  *
700  * Returns the number of jobs that can be prunned or purged.
701  *
702  */
703 int exclude_running_jobs_from_list(del_ctx *prune_list)
704 {
705    int count = 0;
706    JCR *jcr;
707    bool skip;
708    int i;          
709
710    /* Do not prune any job currently running */
711    for (i=0; i < prune_list->num_ids; i++) {
712       skip = false;
713       foreach_jcr(jcr) {
714          if (jcr->JobId == prune_list->JobId[i]) {
715             Dmsg2(050, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
716             prune_list->JobId[i] = 0;
717             skip = true;
718             break;
719          }
720       }
721       endeach_jcr(jcr);
722       if (skip) {
723          continue;  /* don't increment count */
724       }
725       Dmsg2(050, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
726       count++;
727    }
728    return count;
729 }