]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_purge.c
3f6726016a9157109c1b0e827da64bbeb95b9a72
[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;
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    } else {
487       /*
488        * Purge ALL JobIds
489        */
490       if (!db_get_volume_jobids(ua->jcr, ua->db, mr, &lst)) {
491          ua->error_msg("%s", db_strerror(ua->db));
492          Dmsg0(050, "Count failed\n");
493          goto bail_out;
494       }
495       jobids = lst.list;
496    }
497
498    if (*jobids) {
499       purge_jobs_from_catalog(ua, jobids);
500    }
501
502    ua->info_msg(_("%d Job%s on Volume \"%s\" purged from catalog.\n"),
503                 lst.count, lst.count<=1?"":"s", mr->VolumeName);
504
505    purged = is_volume_purged(ua, mr, force);
506
507 bail_out:
508    return purged;
509 }
510
511 /*
512  * This routine will check the JobMedia records to see if the
513  *   Volume has been purged. If so, it marks it as such and
514  *
515  * Returns: true if volume purged
516  *          false if not
517  *
518  * Note, we normally will not purge a volume that has Firstor LastWritten
519  *   zero, because it means the volume is most likely being written
520  *   however, if the user manually purges using the purge command in
521  *   the console, he has been warned, and we go ahead and purge
522  *   the volume anyway, if possible).
523  */
524 bool is_volume_purged(UAContext *ua, MEDIA_DBR *mr, bool force)
525 {
526    POOL_MEM query(PM_MESSAGE);
527    struct s_count_ctx cnt;
528    bool purged = false;
529    char ed1[50];
530
531    if (!force && (mr->FirstWritten == 0 || mr->LastWritten == 0)) {
532       goto bail_out;               /* not written cannot purge */
533    }
534
535    if (strcmp(mr->VolStatus, "Purged") == 0) {
536       Dmsg1(100, "Volume=%s already purged.\n", mr->VolumeName);
537       purged = true;
538       goto bail_out;
539    }
540
541    /* If purged, mark it so */
542    cnt.count = 0;
543    Mmsg(query, "SELECT 1 FROM JobMedia WHERE MediaId=%s LIMIT 1",
544         edit_int64(mr->MediaId, ed1));
545    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
546       ua->error_msg("%s", db_strerror(ua->db));
547       Dmsg0(050, "Count failed\n");
548       goto bail_out;
549    }
550
551    if (cnt.count == 0) {
552       ua->warning_msg(_("There are no more Jobs associated with Volume \"%s\". Marking it purged.\n"),
553          mr->VolumeName);
554       Dmsg1(100, "There are no more Jobs associated with Volume \"%s\". Marking it purged.\n",
555          mr->VolumeName);
556       if (!(purged = mark_media_purged(ua, mr))) {
557          ua->error_msg("%s", db_strerror(ua->db));
558       }
559    }
560 bail_out:
561    return purged;
562 }
563
564 /*
565  * Called here to send the appropriate commands to the SD
566  *  to do truncate on purge.
567  */
568 static void truncate_volume(UAContext *ua, MEDIA_DBR *mr,
569                             char *pool, char *storage,
570                             int drive, BSOCK *sd)
571 {
572    bool ok = false;
573    uint64_t VolBytes = 0;
574    uint64_t VolABytes = 0;
575    uint32_t VolType = 0;
576
577    if (!mr->Recycle) {
578       return;
579    }
580
581    /* Do it only if action on purge = truncate is set */
582    if (!(mr->ActionOnPurge & ON_PURGE_TRUNCATE)) {
583       ua->error_msg(_("\nThe option \"Action On Purge = Truncate\" was not defined in the Pool resource.\n"
584                       "Unable to truncate volume \"%s\"\n"), mr->VolumeName);
585       return;
586    }
587
588    /*
589     * Send the command to truncate the volume after purge. If this feature
590     * is disabled for the specific device, this will be a no-op.
591     */
592
593    /* Protect us from spaces */
594    bash_spaces(mr->VolumeName);
595    bash_spaces(mr->MediaType);
596    bash_spaces(pool);
597    bash_spaces(storage);
598
599    /* Do it by relabeling the Volume, which truncates it */
600    sd->fsend("relabel %s OldName=%s NewName=%s PoolName=%s "
601              "MediaType=%s Slot=%d drive=%d\n",
602              storage,
603              mr->VolumeName, mr->VolumeName,
604              pool, mr->MediaType, mr->Slot, drive);
605
606    unbash_spaces(mr->VolumeName);
607    unbash_spaces(mr->MediaType);
608    unbash_spaces(pool);
609    unbash_spaces(storage);
610
611    /* Check for valid response. With cloud volumes, the upload of the part.1 can
612     * generate a dir_update_volume_info() message that is handled by bget_dirmsg()
613     */
614    while (bget_dirmsg(sd) >= 0) {
615       ua->send_msg("%s", sd->msg);
616       if (sscanf(sd->msg, "3000 OK label. VolBytes=%llu VolABytes=%lld VolType=%d ",
617                  &VolBytes, &VolABytes, &VolType) == 3) {
618
619          ok=true;
620          mr->VolBytes = VolBytes;
621          mr->VolABytes = VolABytes;
622          mr->VolType = VolType;
623          mr->VolFiles = 0;
624          mr->VolParts = 1;
625          mr->VolCloudParts = 0;
626          mr->LastPartBytes = VolBytes;
627
628          set_storageid_in_mr(NULL, mr);
629          if (!db_update_media_record(ua->jcr, ua->db, mr)) {
630             ua->error_msg(_("Can't update volume size in the catalog\n"));
631          }
632          ua->send_msg(_("The volume \"%s\" has been truncated\n"), mr->VolumeName);
633       }
634    }
635    if (!ok) {
636       ua->warning_msg(_("Unable to truncate volume \"%s\"\n"), mr->VolumeName);
637    }
638 }
639
640 /*
641  * Implement Bacula bconsole command  purge action
642  *     purge action=truncate pool= volume= storage= mediatype=
643  * or
644  *     truncate [cache] pool= volume= storage= mediatype=
645  *
646  * If the keyword "cache:  is present, then we use the truncate
647  *   command rather than relabel so that the driver can decide
648  *   whether or not it wants to truncate.  Note: only the
649  *   Cloud driver permits truncating the cache.
650  *
651  * Note, later we might want to rename this action_on_purge_cmd() as
652  *  was the original, but only if we add additional actions such as
653  *  erase, ... For the moment, we only do a truncate.
654  *
655  */
656 int truncate_cmd(UAContext *ua, const char *cmd)
657 {
658    int drive = -1;
659    int nb = 0;
660    uint32_t *results = NULL;
661    const char *action = "truncate";
662    MEDIA_DBR mr;
663    POOL_DBR pr;
664    BSOCK *sd;
665    char storage[MAX_NAME_LENGTH];
666
667    if (find_arg(ua, "cache") > 0) {
668       return cloud_volumes_cmd(ua, cmd, "truncate cache");
669    }
670    
671    memset(&pr, 0, sizeof(pr));
672
673    /*
674     * Look for all Purged volumes that can be recycled, are enabled and
675     *  have more the 10,000 bytes.
676     */
677    mr.Recycle = 1;
678    mr.Enabled = 1;
679    mr.VolBytes = 200;
680    bstrncpy(mr.VolStatus, "Purged", sizeof(mr.VolStatus));
681
682    if (!scan_storage_cmd(ua, cmd, true, /* allfrompool */
683                          &drive, &mr, &pr, &action, storage, &nb, &results)) {
684       goto bail_out;
685    }
686
687    if ((sd=open_sd_bsock(ua)) == NULL) {
688       Dmsg0(100, "Can't open connection to sd\n");
689       goto bail_out;
690    }
691
692    /*
693     * Loop over the candidate Volumes and actually truncate them
694     */
695    for (int i=0; i < nb; i++) {
696       mr.clear();
697       mr.MediaId = results[i];
698       if (db_get_media_record(ua->jcr, ua->db, &mr)) {
699          if (drive < 0) {
700             STORE *store = (STORE*)GetResWithName(R_STORAGE, storage);
701             drive = get_storage_drive(ua, store);
702          }
703
704          /* Must select Pool if not already done */
705          if (pr.PoolId == 0) {
706             pr.PoolId = mr.PoolId;
707             if (!db_get_pool_record(ua->jcr, ua->db, &pr)) {
708                return 1;
709             }
710          }
711          if (strcasecmp("truncate", action) == 0) {
712             truncate_volume(ua, &mr, pr.Name, storage,
713                             drive, sd);
714          }
715       } else {
716          Dmsg1(0, "Can't find MediaId=%lu\n", mr.MediaId);
717       }
718    }
719
720 bail_out:
721    close_db(ua);
722    close_sd_bsock(ua);
723    ua->jcr->wstore = NULL;
724    if (results) {
725       free(results);
726    }
727
728    return 1;
729 }
730
731 /*
732  * IF volume status is Append, Full, Used, or Error, mark it Purged
733  *   Purged volumes can then be recycled (if enabled).
734  */
735 bool mark_media_purged(UAContext *ua, MEDIA_DBR *mr)
736 {
737    JCR *jcr = ua->jcr;
738    if (strcmp(mr->VolStatus, "Append") == 0 ||
739        strcmp(mr->VolStatus, "Full")   == 0 ||
740        strcmp(mr->VolStatus, "Used")   == 0 ||
741        strcmp(mr->VolStatus, "Error")  == 0) {
742       bstrncpy(mr->VolStatus, "Purged", sizeof(mr->VolStatus));
743       set_storageid_in_mr(NULL, mr);
744       if (!db_update_media_record(jcr, ua->db, mr)) {
745          return false;
746       }
747       pm_strcpy(jcr->VolumeName, mr->VolumeName);
748       generate_plugin_event(jcr, bDirEventVolumePurged);
749       /*
750        * If the RecyclePool is defined, move the volume there
751        */
752       if (mr->RecyclePoolId && mr->RecyclePoolId != mr->PoolId) {
753          POOL_DBR oldpr, newpr;
754          memset(&oldpr, 0, sizeof(POOL_DBR));
755          memset(&newpr, 0, sizeof(POOL_DBR));
756          newpr.PoolId = mr->RecyclePoolId;
757          oldpr.PoolId = mr->PoolId;
758          if (   db_get_pool_numvols(jcr, ua->db, &oldpr)
759              && db_get_pool_numvols(jcr, ua->db, &newpr)) {
760             /* check if destination pool size is ok */
761             if (newpr.MaxVols > 0 && newpr.NumVols >= newpr.MaxVols) {
762                ua->error_msg(_("Unable move recycled Volume in full "
763                               "Pool \"%s\" MaxVols=%d\n"),
764                         newpr.Name, newpr.MaxVols);
765
766             } else {            /* move media */
767                update_vol_pool(ua, newpr.Name, mr, &oldpr);
768             }
769          } else {
770             ua->error_msg("%s", db_strerror(ua->db));
771          }
772       }
773
774       /* Send message to Job report, if it is a *real* job */
775       if (jcr && jcr->JobId > 0) {
776          Jmsg(jcr, M_INFO, 0, _("All records pruned from Volume \"%s\"; marking it \"Purged\"\n"),
777             mr->VolumeName);
778       }
779       return true;
780    } else {
781       ua->error_msg(_("Cannot purge Volume with VolStatus=%s\n"), mr->VolStatus);
782    }
783    return strcmp(mr->VolStatus, "Purged") == 0;
784 }