]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
Ensure that JOB_DBR is properly initialized before a db_list_job_records()
[bacula/bacula] / bacula / src / dird / ua_purge.c
1 /*
2    Bacula(R) - The Network Backup Solution
3
4    Copyright (C) 2000-2015 Kern Sibbald
5    Copyright (C) 2002-2014 Free Software Foundation Europe e.V.
6
7    The original author of Bacula is Kern Sibbald, with contributions
8    from many others, a complete list can be found in the file AUTHORS.
9
10    You may use this file and others of this release according to the
11    license defined in the LICENSE file, which includes the Affero General
12    Public License, v3.0 ("AGPLv3") and some additional permissions and
13    terms pursuant to its AGPLv3 Section 7.
14
15    This notice must be preserved when any source code is 
16    conveyed and/or propagated.
17
18    Bacula(R) is a registered trademark of Kern Sibbald.
19 */
20 /*
21  *
22  *   Bacula Director -- User Agent Database Purge Command
23  *
24  *      Purges Files from specific JobIds
25  * or
26  *      Purges Jobs from Volumes
27  *
28  *     Kern Sibbald, February MMII
29  *
30  */
31
32 #include "bacula.h"
33 #include "dird.h"
34
35 /* Forward referenced functions */
36 static int purge_files_from_client(UAContext *ua, CLIENT *client);
37 static int purge_jobs_from_client(UAContext *ua, CLIENT *client);
38 int truncate_cmd(UAContext *ua, const char *cmd);
39
40 static const char *select_jobsfiles_from_client =
41    "SELECT JobId FROM Job "
42    "WHERE ClientId=%s "
43    "AND PurgedFiles=0";
44
45 static const char *select_jobs_from_client =
46    "SELECT JobId, PurgedFiles FROM Job "
47    "WHERE ClientId=%s";
48
49 /*
50  *   Purge records from database
51  *
52  *     Purge Files (from) [Job|JobId|Client|Volume]
53  *     Purge Jobs  (from) [Client|Volume]
54  *     Purge Volumes
55  *
56  *  N.B. Not all above is implemented yet.
57  */
58 int purge_cmd(UAContext *ua, const char *cmd)
59 {
60    int i;
61    CLIENT *client;
62    MEDIA_DBR mr;
63    JOB_DBR  jr;
64    memset(&jr, 0, sizeof(jr));
65
66    static const char *keywords[] = {
67       NT_("files"),
68       NT_("jobs"),
69       NT_("volume"),
70       NULL};
71
72    static const char *files_keywords[] = {
73       NT_("Job"),
74       NT_("JobId"),
75       NT_("Client"),
76       NT_("Volume"),
77       NULL};
78
79    static const char *jobs_keywords[] = {
80       NT_("Client"),
81       NT_("Volume"),
82       NULL};
83
84    /* Special case for the "Action On Purge", this option is working only on
85     * Purged volume, so no jobs or files will be purged.
86     * We are skiping this message if "purge volume action=xxx"
87     */
88    if (!(find_arg(ua, "volume") >= 0 && find_arg(ua, "action") >= 0)) {
89       ua->warning_msg(_(
90         "\nThis command can be DANGEROUS!!!\n\n"
91         "It purges (deletes) all Files from a Job,\n"
92         "JobId, Client or Volume; or it purges (deletes)\n"
93         "all Jobs from a Client or Volume without regard\n"
94         "to retention periods. Normally you should use the\n"
95         "PRUNE command, which respects retention periods.\n"));
96    }
97
98    if (!open_new_client_db(ua)) {
99       return 1;
100    }
101    switch (find_arg_keyword(ua, keywords)) {
102    /* Files */
103    case 0:
104       switch(find_arg_keyword(ua, files_keywords)) {
105       case 0:                         /* Job */
106       case 1:                         /* JobId */
107          if (get_job_dbr(ua, &jr)) {
108             char jobid[50];
109             edit_int64(jr.JobId, jobid);
110             purge_files_from_jobs(ua, jobid);
111          }
112          return 1;
113       case 2:                         /* client */
114          client = get_client_resource(ua);
115          if (client) {
116             purge_files_from_client(ua, client);
117          }
118          return 1;
119       case 3:                         /* Volume */
120          if (select_media_dbr(ua, &mr)) {
121             purge_files_from_volume(ua, &mr);
122          }
123          return 1;
124       }
125    /* Jobs */
126    case 1:
127       switch(find_arg_keyword(ua, jobs_keywords)) {
128       case 0:                         /* client */
129          client = get_client_resource(ua);
130          if (client) {
131             purge_jobs_from_client(ua, client);
132          }
133          return 1;
134       case 1:                         /* Volume */
135          if (select_media_dbr(ua, &mr)) {
136             purge_jobs_from_volume(ua, &mr, /*force*/true);
137          }
138          return 1;
139       }
140    /* Volume */
141    case 2:
142       /* Perform ActionOnPurge (action=truncate) */
143       if (find_arg(ua, "action") >= 0) {
144          return truncate_cmd(ua, ua->cmd);
145       }
146
147       while ((i=find_arg(ua, NT_("volume"))) >= 0) {
148          if (select_media_dbr(ua, &mr)) {
149             purge_jobs_from_volume(ua, &mr, /*force*/true);
150          }
151          *ua->argk[i] = 0;            /* zap keyword already seen */
152          ua->send_msg("\n");
153       }
154       return 1;
155    default:
156       break;
157    }
158    switch (do_keyword_prompt(ua, _("Choose item to purge"), keywords)) {
159    case 0:                            /* files */
160       client = get_client_resource(ua);
161       if (client) {
162          purge_files_from_client(ua, client);
163       }
164       break;
165    case 1:                            /* jobs */
166       client = get_client_resource(ua);
167       if (client) {
168          purge_jobs_from_client(ua, client);
169       }
170       break;
171    case 2:                            /* Volume */
172       if (select_media_dbr(ua, &mr)) {
173          purge_jobs_from_volume(ua, &mr, /*force*/true);
174       }
175       break;
176    }
177    return 1;
178 }
179
180 /*
181  * Purge File records from the database. For any Job which
182  * is older than the retention period, we unconditionally delete
183  * all File records for that Job.  This is simple enough that no
184  * temporary tables are needed. We simply make an in memory list of
185  * the JobIds meeting the prune conditions, then delete all File records
186  * pointing to each of those JobIds.
187  */
188 static int purge_files_from_client(UAContext *ua, CLIENT *client)
189 {
190    struct del_ctx del;
191    POOL_MEM query(PM_MESSAGE);
192    CLIENT_DBR cr;
193    char ed1[50];
194
195    memset(&cr, 0, sizeof(cr));
196    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
197    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
198       return 0;
199    }
200
201    memset(&del, 0, sizeof(del));
202    del.max_ids = 1000;
203    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
204
205    ua->info_msg(_("Begin purging files for Client \"%s\"\n"), cr.Name);
206
207    Mmsg(query, select_jobsfiles_from_client, edit_int64(cr.ClientId, ed1));
208    Dmsg1(050, "select sql=%s\n", query.c_str());
209    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
210
211    purge_files_from_job_list(ua, del);
212
213    if (del.num_ids == 0) {
214       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
215          client->name(), client->catalog->name());
216    } else {
217       ua->info_msg(_("Files for %d Jobs for client \"%s\" purged from %s catalog.\n"), del.num_ids,
218          client->name(), client->catalog->name());
219    }
220
221    if (del.JobId) {
222       free(del.JobId);
223    }
224    return 1;
225 }
226
227
228
229 /*
230  * Purge Job records from the database. For any Job which
231  * is older than the retention period, we unconditionally delete
232  * it and all File records for that Job.  This is simple enough that no
233  * temporary tables are needed. We simply make an in memory list of
234  * the JobIds then delete the Job, Files, and JobMedia records in that list.
235  */
236 static int purge_jobs_from_client(UAContext *ua, CLIENT *client)
237 {
238    struct del_ctx del;
239    POOL_MEM query(PM_MESSAGE);
240    CLIENT_DBR cr;
241    char ed1[50];
242
243    memset(&cr, 0, sizeof(cr));
244
245    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
246    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
247       return 0;
248    }
249
250    memset(&del, 0, sizeof(del));
251    del.max_ids = 1000;
252    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
253    del.PurgedFiles = (char *)malloc(del.max_ids);
254
255    ua->info_msg(_("Begin purging jobs from Client \"%s\"\n"), cr.Name);
256
257    Mmsg(query, select_jobs_from_client, edit_int64(cr.ClientId, ed1));
258    Dmsg1(150, "select sql=%s\n", query.c_str());
259    db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del);
260
261    purge_job_list_from_catalog(ua, del);
262
263    if (del.num_ids == 0) {
264       ua->warning_msg(_("No Files found for client %s to purge from %s catalog.\n"),
265          client->name(), client->catalog->name());
266    } else {
267       ua->info_msg(_("%d Jobs for client %s purged from %s catalog.\n"), del.num_ids,
268          client->name(), client->catalog->name());
269    }
270
271    if (del.JobId) {
272       free(del.JobId);
273    }
274    if (del.PurgedFiles) {
275       free(del.PurgedFiles);
276    }
277    return 1;
278 }
279
280
281 /*
282  * Remove File records from a list of JobIds
283  */
284 void purge_files_from_jobs(UAContext *ua, char *jobs)
285 {
286    POOL_MEM query(PM_MESSAGE);
287
288    Mmsg(query, "DELETE FROM File WHERE JobId IN (%s)", jobs);
289    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
290    Dmsg1(050, "Delete File sql=%s\n", query.c_str());
291
292    Mmsg(query, "DELETE FROM BaseFiles WHERE JobId IN (%s)", jobs);
293    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
294    Dmsg1(050, "Delete BaseFiles sql=%s\n", query.c_str());
295
296    Mmsg(query, "DELETE FROM PathVisibility WHERE JobId IN (%s)", jobs);
297    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
298    Dmsg1(050, "Delete PathVisibility sql=%s\n", query.c_str());
299
300    /*
301     * Now mark Job as having files purged. This is necessary to
302     * avoid having too many Jobs to process in future prunings. If
303     * we don't do this, the number of JobId's in our in memory list
304     * could grow very large.
305     */
306    Mmsg(query, "UPDATE Job SET PurgedFiles=1 WHERE JobId IN (%s)", jobs);
307    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
308    Dmsg1(050, "Mark purged sql=%s\n", query.c_str());
309 }
310
311 /*
312  * Delete jobs (all records) from the catalog in groups of 1000
313  *  at a time.
314  */
315 void purge_job_list_from_catalog(UAContext *ua, del_ctx &del)
316 {
317    POOL_MEM jobids(PM_MESSAGE);
318    char ed1[50];
319
320    for (int i=0; del.num_ids; ) {
321       Dmsg1(150, "num_ids=%d\n", del.num_ids);
322       pm_strcat(jobids, "");
323       for (int j=0; j<1000 && del.num_ids>0; j++) {
324          del.num_ids--;
325          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
326             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
327             i++;
328             continue;
329          }
330          if (*jobids.c_str() != 0) {
331             pm_strcat(jobids, ",");
332          }
333          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
334          Dmsg1(150, "Add id=%s\n", ed1);
335          del.num_del++;
336       }
337       Dmsg1(150, "num_ids=%d\n", del.num_ids);
338       purge_jobs_from_catalog(ua, jobids.c_str());
339    }
340 }
341
342 /*
343  * Delete files from a list of jobs in groups of 1000
344  *  at a time.
345  */
346 void purge_files_from_job_list(UAContext *ua, del_ctx &del)
347 {
348    POOL_MEM jobids(PM_MESSAGE);
349    char ed1[50];
350    /*
351     * OK, now we have the list of JobId's to be pruned, send them
352     *   off to be deleted batched 1000 at a time.
353     */
354    for (int i=0; del.num_ids; ) {
355       pm_strcat(jobids, "");
356       for (int j=0; j<1000 && del.num_ids>0; j++) {
357          del.num_ids--;
358          if (del.JobId[i] == 0 || ua->jcr->JobId == del.JobId[i]) {
359             Dmsg2(150, "skip JobId[%d]=%d\n", i, (int)del.JobId[i]);
360             i++;
361             continue;
362          }
363          if (*jobids.c_str() != 0) {
364             pm_strcat(jobids, ",");
365          }
366          pm_strcat(jobids, edit_int64(del.JobId[i++], ed1));
367          Dmsg1(150, "Add id=%s\n", ed1);
368          del.num_del++;
369       }
370       purge_files_from_jobs(ua, jobids.c_str());
371    }
372 }
373
374 /*
375  * Change the type of the next copy job to backup.
376  * We need to upgrade the next copy of a normal job,
377  * and also upgrade the next copy when the normal job
378  * already have been purged.
379  *
380  *   JobId: 1   PriorJobId: 0    (original)
381  *   JobId: 2   PriorJobId: 1    (first copy)
382  *   JobId: 3   PriorJobId: 1    (second copy)
383  *
384  *   JobId: 2   PriorJobId: 1    (first copy, now regular backup)
385  *   JobId: 3   PriorJobId: 1    (second copy)
386  *
387  *  => Search through PriorJobId in jobid and
388  *                    PriorJobId in PriorJobId (jobid)
389  */
390 void upgrade_copies(UAContext *ua, char *jobs)
391 {
392    POOL_MEM query(PM_MESSAGE);
393    int dbtype = ua->db->bdb_get_type_index();
394
395    db_lock(ua->db);
396    
397    Mmsg(query, uap_upgrade_copies_oldest_job[dbtype], JT_JOB_COPY, jobs, jobs);
398    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
399    Dmsg1(050, "Upgrade copies Log sql=%s\n", query.c_str());
400
401    /* Now upgrade first copy to Backup */
402    Mmsg(query, "UPDATE Job SET Type='B' "      /* JT_JOB_COPY => JT_BACKUP  */
403                 "WHERE JobId IN ( SELECT JobId FROM cpy_tmp )");
404
405    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
406
407    Mmsg(query, "DROP TABLE cpy_tmp");
408    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
409
410    db_unlock(ua->db);
411 }
412
413 /*
414  * Remove all records from catalog for a list of JobIds
415  */
416 void purge_jobs_from_catalog(UAContext *ua, char *jobs)
417 {
418    POOL_MEM query(PM_MESSAGE);
419
420    /* Delete (or purge) records associated with the job */
421    purge_files_from_jobs(ua, jobs);
422
423    Mmsg(query, "DELETE FROM JobMedia WHERE JobId IN (%s)", jobs);
424    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
425    Dmsg1(050, "Delete JobMedia sql=%s\n", query.c_str());
426
427    Mmsg(query, "DELETE FROM Log WHERE JobId IN (%s)", jobs);
428    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
429    Dmsg1(050, "Delete Log sql=%s\n", query.c_str());
430
431    Mmsg(query, "DELETE FROM RestoreObject WHERE JobId IN (%s)", jobs);
432    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
433    Dmsg1(050, "Delete RestoreObject sql=%s\n", query.c_str());
434
435    /* The JobId of the Snapshot record is no longer usable 
436     * TODO: Migth want to use a copy for the jobid? 
437     */
438    Mmsg(query, "UPDATE Snapshot SET JobId=0 WHERE JobId IN (%s)", jobs);
439    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
440
441    upgrade_copies(ua, jobs);
442
443    /* Now remove the Job record itself */
444    Mmsg(query, "DELETE FROM Job WHERE JobId IN (%s)", jobs);
445    db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL);
446
447    Dmsg1(050, "Delete Job sql=%s\n", query.c_str());
448 }
449
450 void purge_files_from_volume(UAContext *ua, MEDIA_DBR *mr )
451 {} /* ***FIXME*** implement */
452
453 /*
454  * Returns: 1 if Volume purged
455  *          0 if Volume not purged
456  */
457 bool purge_jobs_from_volume(UAContext *ua, MEDIA_DBR *mr, bool force)
458 {
459    POOL_MEM query(PM_MESSAGE);
460    db_list_ctx lst;
461    char *jobids=NULL;
462    int i;
463    bool purged = false;
464    bool stat;
465
466    stat = strcmp(mr->VolStatus, "Append") == 0 ||
467           strcmp(mr->VolStatus, "Full")   == 0 ||
468           strcmp(mr->VolStatus, "Used")   == 0 ||
469           strcmp(mr->VolStatus, "Error")  == 0;
470    if (!stat) {
471       ua->error_msg(_("\nVolume \"%s\" has VolStatus \"%s\" and cannot be purged.\n"
472                      "The VolStatus must be: Append, Full, Used, or Error to be purged.\n"),
473                      mr->VolumeName, mr->VolStatus);
474       return 0;
475    }
476
477    /*
478     * Check if he wants to purge a single jobid
479     */
480    i = find_arg_with_value(ua, "jobid");
481    if (i >= 0 && is_a_number_list(ua->argv[i])) {
482       jobids = ua->argv[i];
483    } else {
484       /*
485        * Purge ALL JobIds
486        */
487       if (!db_get_volume_jobids(ua->jcr, ua->db, mr, &lst)) {
488          ua->error_msg("%s", db_strerror(ua->db));
489          Dmsg0(050, "Count failed\n");
490          goto bail_out;
491       }
492       jobids = lst.list;
493    }
494
495    if (*jobids) {
496       purge_jobs_from_catalog(ua, jobids);
497    }
498
499    ua->info_msg(_("%d File%s on Volume \"%s\" purged from catalog.\n"),
500                 lst.count, lst.count<=1?"":"s", mr->VolumeName);
501
502    purged = is_volume_purged(ua, mr, force);
503
504 bail_out:
505    return purged;
506 }
507
508 /*
509  * This routine will check the JobMedia records to see if the
510  *   Volume has been purged. If so, it marks it as such and
511  *
512  * Returns: true if volume purged
513  *          false if not
514  *
515  * Note, we normally will not purge a volume that has Firstor LastWritten
516  *   zero, because it means the volume is most likely being written
517  *   however, if the user manually purges using the purge command in
518  *   the console, he has been warned, and we go ahead and purge
519  *   the volume anyway, if possible).
520  */
521 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
522 {
523    POOL_MEM query(PM_MESSAGE);
524    struct s_count_ctx cnt;
525    bool purged = false;
526    char ed1[50];
527
528    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
529       goto bail_out;               /* not written cannot purge */
530    }
531
532    if (strcmp(mr->VolStatus, "Purged") == 0) {
533       Dmsg1(100, "Volume=%s already purged.\n", mr->VolumeName);
534       purged = true;
535       goto bail_out;
536    }
537
538    /* If purged, mark it so */
539    cnt.count = 0;
540    Mmsg(query, "SELECT 1 FROM JobMedia WHERE MediaId=%s LIMIT 1",
541         edit_int64(mr->MediaId, ed1));
542    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
543       ua->error_msg("%s", db_strerror(ua->db));
544       Dmsg0(050, "Count failed\n");
545       goto bail_out;
546    }
547
548    if (cnt.count == 0) {
549       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
550          mr->VolumeName);
551       Dmsg1(100, "There are no more Jobs associated with Volume \"%s\". Marking it purged.\n",
552          mr->VolumeName);
553       if (!(purged = mark_media_purged(ua, mr))) {
554          ua->error_msg("%s", db_strerror(ua->db));
555       }
556    }
557 bail_out:
558    return purged;
559 }
560
561 /*
562  * Called here to send the appropriate commands to the SD
563  *  to do truncate on purge.
564  */
565 static void do_truncate_on_purge(UAContext *ua, MEDIA_DBR *mr,
566                                  char *pool, char *storage,
567                                  int drive, BSOCK *sd)
568 {
569    bool ok = false;
570    uint64_t VolBytes = 0;
571    uint64_t VolABytes = 0;
572    uint32_t VolType = 0;
573
574    /* TODO: Return if not mr->Recyle ? */
575    if (!mr->Recycle) {
576       return;
577    }
578
579    /* Do it only if action on purge = truncate is set */
580    if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
581       return;
582    }
583    /*
584     * Send the command to truncate the volume after purge. If this feature
585     * is disabled for the specific device, this will be a no-op.
586     */
587
588    /* Protect us from spaces */
589    bash_spaces(mr->VolumeName);
590    bash_spaces(mr->MediaType);
591    bash_spaces(pool);
592    bash_spaces(storage);
593
594    /* Do it by relabeling the Volume, which truncates it */
595    sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
596              "MediaType=%s Slot=%d drive=%d\n",
597                 storage,
598                 mr->VolumeName, mr->VolumeName,
599                 pool, mr->MediaType, mr->Slot, drive);
600
601    unbash_spaces(mr->VolumeName);
602    unbash_spaces(mr->MediaType);
603    unbash_spaces(pool);
604    unbash_spaces(storage);
605
606    /* Send relabel command, and check for valid response */
607    while (sd->recv() >= 0) {
608       ua->send_msg("%s", sd->msg);
609       if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu VolABytes=%lld VolType=%d ",
610          &VolBytes, &VolABytes, &VolType) == 3) {
611          ok = true;
612       }
613    }
614
615    if (ok) {
616       mr->VolBytes = VolBytes;
617       mr->VolABytes = VolABytes;
618       mr->VolType = VolType;
619       mr->VolFiles = 0;
620       set_storageid_in_mr(NULL, mr);
621       if (!db_update_media_record(ua->jcr, ua->db, mr)) {
622          ua->error_msg(_("Can't update volume size in the catalog\n"));
623       }
624       ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
625    } else {
626       ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
627    }
628 }
629
630 /*
631  * Implement Bacula bconsole command  purge action
632  *     purge action=truncate pool= volume= storage= mediatype=
633  * or
634  *     truncate pool= volume= storage= mediatype=
635  *
636  * Note, later we might want to rename this action_on_purge_cmd() as
637  *  was the original, but only if we add additional actions such as
638  *  erase, ... For the moment, we only do a truncate.
639  *
640  */
641 int truncate_cmd(UAContext *ua, const char *cmd)
642 {
643    bool allpools = false;
644    int drive = -1;
645    int nb = 0;
646    uint32_t *results = NULL;
647    const char *action = "truncate";
648    STORE *store = NULL;
649    POOL *pool = NULL;
650    MEDIA_DBR mr;
651    POOL_DBR pr;
652    BSOCK *sd = NULL;
653
654    memset(&pr, 0, sizeof(pr));
655
656    /* Look at arguments */
657    for (int i=1; i<ua->argc; i++) {
658       if (strcasecmp(ua->argk[i], NT_("allpools")) == 0) {
659          allpools = true;
660
661       } else if (strcasecmp(ua->argk[i], NT_("volume")) == 0
662                  && is_name_valid(ua->argv[i], NULL)) {
663          bstrncpy(mr.VolumeName, ua->argv[i], sizeof(mr.VolumeName));
664
665       } else if (strcasecmp(ua->argk[i], NT_("mediatype")) == 0
666                  && ua->argv[i]) {
667          bstrncpy(mr.MediaType, ua->argv[i], sizeof(mr.MediaType));
668
669       } else if (strcasecmp(ua->argk[i], NT_("drive")) == 0 && ua->argv[i]) {
670          drive = atoi(ua->argv[i]);
671
672       } else if (strcasecmp(ua->argk[i], NT_("action")) == 0
673                  && is_name_valid(ua->argv[i], NULL)) {
674          action = ua->argv[i];
675       }
676    }
677
678    /* Choose storage */
679    ua->jcr->wstore = store =  get_storage_resource(ua, false);
680    if (!store) {
681       goto bail_out;
682    }
683
684    if (!open_db(ua)) {
685       Dmsg0(100, "Can't open db\n");
686       goto bail_out;
687    }
688
689    if (!allpools) {
690       /* force pool selection */
691       pool = get_pool_resource(ua);
692       if (!pool) {
693          Dmsg0(100, "Can't get pool resource\n");
694          goto bail_out;
695       }
696       bstrncpy(pr.Name, pool->name(), sizeof(pr.Name));
697       if (!db_get_pool_record(ua->jcr, ua->db, &pr)) {
698          Dmsg0(100, "Can't get pool record\n");
699          goto bail_out;
700       }
701       mr.PoolId = pr.PoolId;
702    }
703
704    /*
705     * Look for all Purged volumes that can be recycled, are enabled and
706     *  have more the 10,000 bytes.
707     */
708    mr.Recycle = 1;
709    mr.Enabled = 1;
710    mr.VolBytes = 200;
711    set_storageid_in_mr(store, &mr);
712    bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
713    if (!db_get_media_ids(ua->jcr, ua->db, &mr, &nb, &results)) {
714       Dmsg0(100, "No results from db_get_media_ids\n");
715       goto bail_out;
716    }
717
718    if (!nb) {
719       ua->send_msg(_("No Volumes found to perform \"truncate\" command.\n"));
720       goto bail_out;
721    }
722
723    if ((sd=open_sd_bsock(ua)) == NULL) {
724       Dmsg0(100, "Can't open connection to sd\n");
725       goto bail_out;
726    }
727
728    /*
729     * Loop over the candidate Volumes and actually truncate them
730     */
731    for (int i=0; i < nb; i++) {
732       mr.clear();
733       mr.MediaId = results[i];
734       if (db_get_media_record(ua->jcr, ua->db, &mr)) {
735          /* TODO: ask for drive and change Pool */
736          if (strcasecmp("truncate", action) == 0) {
737             do_truncate_on_purge(ua, &mr, pr.Name, store->dev_name(), drive, sd);
738          }
739       } else {
740          Dmsg1(0, "Can't find MediaId=%lld\n", (uint64_t)mr.MediaId);
741       }
742    }
743
744 bail_out:
745    close_db(ua);
746    close_sd_bsock(ua);
747    ua->jcr->wstore = NULL;
748    if (results) {
749       free(results);
750    }
751
752    return 1;
753 }
754
755 /*
756  * IF volume status is Append, Full, Used, or Error, mark it Purged
757  *   Purged volumes can then be recycled (if enabled).
758  */
759 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
760 {
761    JCR *jcr = ua->jcr;
762    if (strcmp(mr->VolStatus, "Append") == 0 ||
763        strcmp(mr->VolStatus, "Full")   == 0 ||
764        strcmp(mr->VolStatus, "Used")   == 0 ||
765        strcmp(mr->VolStatus, "Error")  == 0) {
766       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
767       set_storageid_in_mr(NULL, mr);
768       if (!db_update_media_record(jcr, ua->db, mr)) {
769          return false;
770       }
771       pm_strcpy(jcr->VolumeName, mr->VolumeName);
772       generate_plugin_event(jcr, bDirEventVolumePurged);
773       /*
774        * If the RecyclePool is defined, move the volume there
775        */
776       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
777          POOL_DBR oldpr, newpr;
778          memset(&oldpr, 0, sizeof(POOL_DBR));
779          memset(&newpr, 0, sizeof(POOL_DBR));
780          newpr.PoolId = mr->RecyclePoolId;
781          oldpr.PoolId = mr->PoolId;
782          if (   db_get_pool_numvols(jcr, ua->db, &oldpr)
783              && db_get_pool_numvols(jcr, ua->db, &newpr)) {
784             /* check if destination pool size is ok */
785             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
786                ua->error_msg(_("Unable move recycled Volume in full "
787                               "Pool \"%s\" MaxVols=%d\n"),
788                         newpr.Name, newpr.MaxVols);
789
790             } else {            /* move media */
791                update_vol_pool(ua, newpr.Name, mr, &oldpr);
792             }
793          } else {
794             ua->error_msg("%s", db_strerror(ua->db));
795          }
796       }
797
798       /* Send message to Job report, if it is a *real* job */
799       if (jcr && jcr->JobId > 0) {
800          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
801             mr->VolumeName);
802       }
803       return true;
804    } else {
805       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
806    }
807    return strcmp(mr->VolStatus, "Purged") == 0;
808 }