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