1 # Copyright (c) 2009-2012 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
20 package Amanda::Holding;
22 use base qw( Exporter );
26 use POSIX qw( :fcntl_h );
31 use Amanda::Config qw( :getconf );
32 use Amanda::Debug qw( debug );
39 Amanda::Holding -- interface to the holding disks
48 for my $hfile (Amanda::Holding::files()) {
49 my $hdr = Amanda::Holding::get_header($hfile);
51 $size_per_host{$hdr->{'name'}} += Amanda::Holding::file_size($hfile);
54 Schematic for something like C<amflush>:
56 for my $ts (sort Amanda::Holding::get_all_timestamps()) {
60 for my $hfile (Amanda::Holding::get_files_for_flush(@to_dump)) {
72 A holding disk is a directory given in a holdingdisk definition in
75 =item Holding directory
77 A holding directory is a subdirectory of a holding disk, generally named by
78 timestamp. Note, however, that this package does not interpret holding
79 directory names as timestamps, and does not provide direct access to holding
84 A holding file describes one or more os-level files (holding file chunks) in a
85 holding directory, together representing a single dump file.
89 A holding chunk is an individual os-level file representing part of a holding
90 file. Chunks are kept small to avoid hitting filesystem size ilmits, and are
91 linked together internally by filename.
97 /data/holding <-- holding disk
98 /data/holding/20070306123456 <-- holding directory
99 /data/holding/20070306123456/raj._video_a <-- holding file and chunk
100 /data/holding/20070306123456/raj._video_a.1 <-- holding chunk
104 Holding-disk files do not have a block size, so the size of the header is fixed
105 at 32k. Rather than hard-code that value, use the constant DISK_BLOCK_BYTES
110 Note that this package assumes that a config has been loaded (see
113 These three functions provide basic access to holding disks, files, and chunks:
119 returns an list of active disks, each represented as a string. This does not
120 return holding disks which are defined in C<amanda.conf> but not used.
124 returns a list of active holding files on all disks. Note that a dump may span
125 multiple disks, so there is no use in selecting files only on certain holding
128 =item C<file_chunks($file)>
130 returns a list of chunks for the given file. Chunk filenames are always fully
135 C<Amanda::Holding> provides a few utility functions on holding files. Note
136 that these functions require fully qualified pathnames.
140 =item C<file_size($file, $ignore_headers)>
142 returns the size of the holding file I<in kilobytes>, ignoring the size of the
143 headers if C<$ignore_headers> is true.
145 =item C<file_unlink($file)>
147 unlinks (deletes) all chunks comprising C<$file>, returning true on success.
149 =item C<get_header($file)>
151 reads and returns the header (see L<Amanda::Header>) for C<$file>.
155 The remaining two functions are utilities for amflush and related tools:
159 =item C<get_all_timestamps()>
161 returns a sorted list of all timestamps with dumps in any active holding disk.
163 =item C<get_files_for_flush(@timestamps)>
165 returns a sorted list of files matching any of the supplied timestamps. Files
166 for which no DLE exists in the disklist are ignored. If no timestamps are
167 provided, then all timestamps are considered.
173 use constant DISK_BLOCK_BYTES => 32768;
175 our @EXPORT_OK = qw(dirs files file_chunks
176 get_files_for_flush get_all_datestamps
177 file_size file_unlink get_header);
186 unless (my ($year, $month, $day, $hour, $min, $sec) =
187 ($str =~ /(\d{4})(\d{2})(\d{2})(?:(\d{2})(\d{2})(\d{2}))/));
189 return 0 if ($year < 1990 || $year > 2999);
190 return 0 if ($month < 1 || $month > 12);
191 return 0 if ($day < 1 || $day > 31);
193 return 0 if (defined $hour and $hour > 23);
194 return 0 if (defined $min and $min > 60);
195 return 0 if (defined $sec and $sec > 60);
201 my ($file_fn, $verbose) = @_;
203 # walk disks, directories, and files with nested loops
204 for my $disk (disks()) {
205 my $diskh = IO::Dir->new($disk);
206 if (!defined $diskh) {
207 print $verbose "could not open holding dir '$disk': $!\n" if $verbose;
211 while (defined(my $datestr = $diskh->read())) {
212 next if $datestr eq '.' or $datestr eq '..';
214 my $dirfn = File::Spec->catfile($disk, $datestr);
216 if (!_is_datestr($datestr)) {
217 print $verbose "holding dir '$dirfn' is not a datestamp\n" if $verbose;
221 print $verbose "holding dir '$dirfn' is not a directory\n" if $verbose;
225 my $dirh = IO::Dir->new($dirfn);
226 while (defined(my $dirent = $dirh->read)) {
227 next if $dirent eq '.' or $dirent eq '..';
229 my $filename = File::Spec->catfile($disk, $datestr, $dirent);
231 print $verbose "holding file '$filename' is not a file\n" if $verbose;
235 my $hdr = get_header($filename);
236 next unless defined($hdr);
238 $file_fn->($filename, $hdr);
250 for my $hdname (@{getconf($CNF_HOLDINGDISK)}) {
251 my $cfg = lookup_holdingdisk($hdname);
252 next unless defined $cfg;
254 my $dir = holdingdisk_getconf($cfg, $HOLDING_DISKDIR);
255 next unless defined $dir;
267 my $each_file_fn = sub {
268 my ($filename, $header) = @_;
269 return if $header->{'type'} != $Amanda::Header::F_DUMPFILE;
271 push @results, $filename;
273 _walk($each_file_fn, $verbose);
282 my $each_file_fn = sub {
283 my ($filename, $header) = @_;
284 push @results, { filename => $filename, header => $header };
286 _walk($each_file_fn, $verbose);
291 sub merge_all_files {
296 for my $file (@files) {
297 $hfiles{$file->{'filename'}} = $file->{'header'};
300 foreach my $filename (keys %hfiles) {
301 next if !exists $hfiles{$filename};
302 if ($hfiles{$filename}->{'type'} == $Amanda::Header::F_DUMPFILE) {
303 push @result, {filename => $filename, header => $hfiles{$filename}};
304 my $is_tmp = ($filename =~ /\.tmp$/);
305 my $cont_filename = $filename;
306 my $cfilename = $hfiles{$cont_filename}->{'cont_filename'};
308 $cf .= ".tmp" if $is_tmp;
309 while (defined $cfilename && $cfilename ne "" && -f $cf) {
310 delete $hfiles{$cont_filename};
311 $cont_filename = $cf;
312 $cfilename = $hfiles{$cont_filename}->{'cont_filename'};
314 $cf .= ".tmp" if $is_tmp;
316 delete $hfiles{$cont_filename};
317 } elsif ($hfiles{$filename}->{'type'} != $Amanda::Header::F_CONT_DUMPFILE) {
318 push @result, {filename => $filename, header => $hfiles{$filename}};
319 delete $hfiles{$filename}
321 # do nothing for F_CONTFILE
325 foreach my $filename (keys %hfiles) {
326 next if !exists $hfiles{$filename};
327 if ($hfiles{$filename}->{'type'} == $Amanda::Header::F_CONT_DUMPFILE) {
328 push @result, {filename => $filename, header => $hfiles{$filename}};
330 delete $hfiles{$filename}
341 last unless -f $filename;
342 my $hdr = get_header($filename);
343 last unless defined($hdr);
345 push @results, $filename;
347 if ($hdr->{'cont_filename'}) {
348 $filename = $hdr->{'cont_filename'};
350 # no continuation -> we're done
358 sub file_tmp_chunks {
363 last unless -f $filename;
364 my $hdr = get_header($filename);
365 last unless defined($hdr);
367 push @results, $filename;
369 if ($hdr->{'cont_filename'}) {
370 $filename = $hdr->{'cont_filename'} . ".tmp";
372 # no continuation -> we're done
381 my ($filename) = shift;
382 my ($complete) = shift;
384 my @files = file_tmp_chunks($filename);
385 while (my $tmp_filename = pop @files) {
386 my $hdr = get_header($tmp_filename);
387 if ($hdr->{'is_partial'} == 0 and $complete == 0) {
388 $hdr->{'is_partial'} = 1;
389 write_header($tmp_filename, $hdr);
391 my $hfilename = $tmp_filename;
392 $hfilename =~ s/\.tmp$//;
393 rename $tmp_filename, $hfilename;
401 return unless -f $filename;
403 my $fd = POSIX::open($filename, O_RDONLY);
406 my $hdr_bytes = Amanda::Util::full_read($fd, DISK_BLOCK_BYTES);
408 if (length($hdr_bytes) == 0) {
409 my $hdr = Amanda::Header->new();
410 $hdr->{'type'} = $Amanda::Header::F_EMPTY;
412 } elsif (length($hdr_bytes) < DISK_BLOCK_BYTES) {
413 my $hdr = Amanda::Header->new();
414 $hdr->{'type'} = $Amanda::Header::F_UNKNOWN;
418 return Amanda::Header->from_string($hdr_bytes);
422 my $filename = shift;
425 return unless -f $filename;
426 my $fd = POSIX::open($filename, O_RDWR);
428 my $header = $hdr->to_string(DISK_BLOCK_BYTES, DISK_BLOCK_BYTES);
429 Amanda::Util::full_write($fd, $header, DISK_BLOCK_BYTES);
436 for my $chunk (file_chunks($filename)) {
437 unlink($chunk) or return 0;
446 for my $chunk (filetmp_chunks($filename)) {
447 unlink($chunk) or return 0;
454 # walk disks, directories, and files with nested loops
455 for my $disk (disks()) {
456 my $diskh = IO::Dir->new($disk);
457 next unless defined $diskh;
459 while (defined(my $datestr = $diskh->read())) {
460 next if $datestr eq '.' or $datestr eq '..';
462 my $dirfn = File::Spec->catfile($disk, $datestr);
463 next unless _is_datestr($datestr);
464 next unless -d $dirfn;
471 my ($filename, $ignore_headers) = @_;
472 my $total = Math::BigInt->new(0);
474 for my $chunk (file_chunks($filename)) {
475 my $sb = stat($chunk);
476 my $size = Math::BigInt->new($sb->size);
477 $size -= DISK_BLOCK_BYTES if $ignore_headers;
478 $size = ($size + 1023) / 1024;
486 sub get_files_for_flush {
490 my $each_file_fn = sub {
491 my ($filename, $header) = @_;
492 return if $header->{'type'} != $Amanda::Header::F_DUMPFILE;
494 if (@dateargs && !grep { $_ eq $header->{'datestamp'}; } @dateargs) {
498 if (!Amanda::Disklist::get_disk($header->{'name'}, $header->{'disk'})) {
502 push @results, $filename;
504 _walk($each_file_fn, 0);
506 return sort @results;
509 sub get_all_datestamps {
512 my $each_file_fn = sub {
513 my ($filename, $header) = @_;
514 return if $header->{'type'} != $Amanda::Header::F_DUMPFILE;
516 $datestamps{$header->{'datestamp'}} = 1;
518 _walk($each_file_fn, 0);
520 return sort keys %datestamps;