1package Brackup::Target;
2
3use strict;
4use warnings;
5use Brackup::InventoryDatabase;
6use Brackup::TargetBackupStatInfo;
7use Brackup::Util 'tempfile';
8use Brackup::DecryptedFile;
9use Carp qw(croak);
10
11sub new {
12    my ($class, $confsec) = @_;
13    my $self = bless {}, $class;
14    $self->{name} = $confsec->name;
15    $self->{name} =~ s/^TARGET://
16        or die "No target found matching " . $confsec->name;
17    die "Target name must be only a-z, A-Z, 0-9, and _."
18        unless $self->{name} =~ /^\w+/;
19
20    $self->{keep_backups} = $confsec->value("keep_backups");
21    $self->{inv_db} =
22        Brackup::InventoryDatabase->new($confsec->value("inventorydb_file") ||
23                                        $confsec->value("inventory_db") ||
24                                        "$ENV{HOME}/.brackup-target-$self->{name}.invdb",
25                                        $confsec);
26
27    return $self;
28}
29
30sub name {
31    my $self = shift;
32    return $self->{name};
33}
34
35# return hashref of key/value pairs you want returned to you during a restore
36# you should include anything you need to restore.
37# keys must match /^\w+$/
38sub backup_header {
39    return {}
40}
41
42# returns bool
43sub has_chunk {
44    my ($self, $chunk) = @_;
45    die "ERROR: has_chunk not implemented in sub-class $self";
46}
47
48# returns true on success, or returns false or dies otherwise.
49sub store_chunk {
50    my ($self, $chunk) = @_;
51    die "ERROR: store_chunk not implemented in sub-class $self";
52}
53
54# returns true on success, or returns false or dies otherwise.
55sub delete_chunk {
56    my ($self, $chunk) = @_;
57    die "ERROR: delete_chunk not implemented in sub-class $self";
58}
59
60# returns a list of names of all chunks
61sub chunks {
62    my ($self) = @_;
63    die "ERROR: chunks not implemented in sub-class $self";
64}
65
66sub inventory_db {
67    my $self = shift;
68    return $self->{inv_db};
69}
70
71sub add_to_inventory {
72    my ($self, $pchunk, $schunk) = @_;
73    my $key  = $pchunk->inventory_key;
74    my $db = $self->inventory_db;
75    $db->set($key => $schunk->inventory_value);
76}
77
78# return stored chunk, given positioned chunk, or undef.  no
79# need to override this, unless you have a good reason.
80sub stored_chunk_from_inventory {
81    my ($self, $pchunk) = @_;
82    my $key    = $pchunk->inventory_key;
83    my $db     = $self->inventory_db;
84    my $invval = $db->get($key)
85        or return undef;
86    return Brackup::StoredChunk->new_from_inventory_value($pchunk, $invval);
87}
88
89# return a list of TargetBackupStatInfo objects representing the
90# stored backup metafiles on this target.
91sub backups {
92    my ($self) = @_;
93    die "ERROR: backups method not implemented in sub-class $self";
94}
95
96# downloads the given backup name to the current directory (with
97# *.brackup extension)
98sub get_backup {
99    my ($self, $name) = @_;
100    die "ERROR: get_backup method not implemented in sub-class $self";
101}
102
103# deletes the given backup from this target
104sub delete_backup {
105    my ($self, $name) = @_;
106    die "ERROR: delete_backup method not implemented in sub-class $self";
107}
108
109# removes old metafiles from this target
110sub prune {
111    my ($self, %opt) = @_;
112
113    my $keep_backups = $opt{keep_backups} || $self->{keep_backups}
114        or die "ERROR: keep_backups option not set\n";
115    die "ERROR: keep_backups option must be at least 1\n"
116        unless $keep_backups > 0;
117
118    # select backups to delete
119    my (%backups, @backups_to_delete) = ();
120    foreach my $backup_name (map {$_->filename} $self->backups) {
121        $backup_name =~ /^(.+)-\d+$/;
122        $backups{$1} ||= [];
123        push @{ $backups{$1} }, $backup_name;
124    }
125    foreach my $source (keys %backups) {
126        next if $opt{source} && $source ne $opt{source};
127        my @b = reverse sort @{ $backups{$source} };
128        push @backups_to_delete, splice(@b, ($keep_backups > $#b+1) ? $#b+1 : $keep_backups);
129    }
130
131    warn ($opt{dryrun} ? "Pruning:\n" : "Pruned:\n") if $opt{verbose};
132    foreach my $backup_name (@backups_to_delete) {
133        warn "  $backup_name\n" if $opt{verbose};
134        $self->delete_backup($backup_name) unless $opt{dryrun};
135    }
136    return scalar @backups_to_delete;
137}
138
139# removes orphaned chunks in the target
140sub gc {
141    my ($self, %opt) = @_;
142
143    # get all chunks and then loop through metafiles to detect
144    # referenced ones
145    my %chunks = map {$_ => 1} $self->chunks;
146    my $total_chunks = scalar keys %chunks;
147    my $tempfile = +(tempfile())[1];
148    my @backups = $self->backups;
149    BACKUP: foreach my $i (0 .. $#backups) {
150        my $backup = $backups[$i];
151        warn sprintf "Collating chunks from backup %s [%d/%d]\n",
152            $backup->filename, $i+1, scalar(@backups)
153                if $opt{verbose};
154        $self->get_backup($backup->filename, $tempfile);
155        my $decrypted_backup = new Brackup::DecryptedFile(filename => $tempfile);
156        my $parser = Brackup::Metafile->open($decrypted_backup->name);
157        $parser->readline;  # skip header
158        ITEM: while (my $it = $parser->readline) {
159            next ITEM unless $it->{Chunks};
160            my @item_chunks = map { (split /;/)[3] } grep { $_ } split(/\s+/, $it->{Chunks} || "");
161            delete $chunks{$_} for (@item_chunks);
162        }
163    }
164    my @orphaned_chunks = keys %chunks;
165
166    # report orphaned chunks
167    if (@orphaned_chunks && $opt{verbose} && $opt{verbose} >= 2) {
168      warn "Orphaned chunks:\n";
169      warn "  $_\n" for (@orphaned_chunks);
170    }
171
172    # remove orphaned chunks
173    if (@orphaned_chunks && ! $opt{dryrun}) {
174        my $confirm = 'y';
175        if ($opt{interactive}) {
176            printf "Run gc, removing %d/%d orphaned chunks? [y/N] ",
177              scalar @orphaned_chunks, $total_chunks;
178            $confirm = <>;
179        }
180
181        if (lc substr($confirm,0,1) eq 'y') {
182            warn "Removing orphaned chunks\n" if $opt{verbose};
183            $self->delete_chunk($_) for (@orphaned_chunks);
184
185            # delete orphaned chunks from inventory
186            my $inventory_db = $self->inventory_db;
187            while (my ($k, $v) = $inventory_db->each) {
188                $v =~ s/ .*$//;         # strip value back to hash
189                $inventory_db->delete($k) if exists $chunks{$v};
190            }
191        }
192    }
193
194    return wantarray ? ( scalar @orphaned_chunks, $total_chunks ) :  scalar @orphaned_chunks;
195}
196
197
198
1991;
200
201__END__
202
203=head1 NAME
204
205Brackup::Target - describes the destination for a backup
206
207=head1 EXAMPLE
208
209In your ~/.brackup.conf file:
210
211  [TARGET:amazon]
212  type = Amazon
213  aws_access_key_id  = ...
214  aws_secret_access_key =  ....
215
216=head1 GENERAL CONFIG OPTIONS
217
218=over
219
220=item B<type>
221
222The driver for this target type.  The type B<Foo> corresponds to the Perl module
223Brackup::Target::B<Foo>.
224
225The set of targets (and the valid options for type) currently distributed with the
226Brackup core are:
227
228B<Filesystem> -- see L<Brackup::Target::Filesystem> for configuration details
229
230B<Ftp> -- see L<Brackup::Target::Ftp> for configuration details
231
232B<Sftp> -- see L<Brackup::Target::Sftp> for configuration details
233
234B<Amazon> -- see L<Brackup::Target::Amazon> for configuration details
235
236B<Amazon> -- see L<Brackup::Target::CloudFiles> for configuration details
237
238=item B<keep_backups>
239
240The default number of recent backups to keep when running I<brackup-target prune>.
241
242=item B<inventorydb_file>
243
244The location of the L<Brackup::InventoryDatabase> inventory database file for
245this target e.g.
246
247  [TARGET:amazon]
248  type = Amazon
249  aws_access_key_id  = ...
250  aws_secret_access_key =  ...
251  inventorydb_file = /home/bradfitz/.amazon-already-has-these-chunks.db
252
253Only required if you wish to change this from the default, which is
254".brackup-target-TARGETNAME.invdb" in your home directory.
255
256=item B<inventorydb_type>
257
258Dictionary type to use for the inventory database. The dictionary type B<Bar>
259corresponds to the perl module Brackup::Dict::B<Bar>.
260
261The default inventorydb_type is B<SQLite>. See L<Brackup::InventoryDatabase> for
262more.
263
264=item B<inherit>
265
266The name of another Brackup::Target section to inherit from i.e. to use
267for any parameters that are not already defined in the current section e.g.:
268
269  [TARGET:ftp_defaults]
270  type = Ftp
271  ftp_host = myserver
272  ftp_user = myusername
273  ftp_password = mypassword
274
275  [TARGET:ftp_home]
276  inherit = ftp_defaults
277  path = home
278
279  [TARGET:ftp_images]
280  inherit = ftp_defaults
281  path = images
282
283=back
284
285=head1 SEE ALSO
286
287L<Brackup>
288
289L<Brackup::InventoryDatabase>
290
291=cut
292
293# vim:sw=4:et
294
295