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