File Coverage

blib/lib/Dev/Util/Backup.pm
Criterion Covered Total %
statement 86 92 93.4
branch 27 44 61.3
condition 9 24 37.5
subroutine 14 15 93.3
pod 1 1 100.0
total 137 176 77.8


line stmt bran cond sub pod time code
1             package Dev::Util::Backup;
2              
3 2     2   3494 use Dev::Util::Syntax;
  2         5  
  2         17  
4 2     2   13 use Exporter qw(import);
  2         2  
  2         49  
5              
6 2     2   1096 use File::Copy;
  2         5889  
  2         113  
7 2     2   11 use File::Spec;
  2         2  
  2         47  
8 2     2   8 use File::Basename;
  2         3  
  2         112  
9 2     2   8 use File::Find;
  2         2  
  2         97  
10 2     2   386 use IO::File;
  2         1367  
  2         308  
11 2     2   1363 use Archive::Tar;
  2         190026  
  2         2939  
12              
13             our $VERSION = version->declare("v2.19.35");
14              
15             our @EXPORT_OK = qw(
16             backup
17             );
18              
19             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
20              
21             #where to save backup files
22             our $BACKUPDIR = '';
23              
24             #should we preserve atime, mtime, and mode of the original
25             #file in all backups ?
26             our $PRESERVE_FILE_ATTRS = 1;
27              
28             # Backup file or directory
29             sub backup {
30 4 100   4 1 3867 return -f $_[0] ? _backupfile(@_) : _backupdir(@_);
31             }
32              
33             # Backup file -- takes file name and returns new file name
34             # This sub can DIE -- so use eval
35             sub _backupfile {
36 3     3   11 my $filename = shift;
37              
38 3 50       35 croak "$filename is not a file\n" unless ( -e $filename );
39              
40             #backup file will have _yyyymmdd extention added to it
41 3         11 my $newfile = _backup($filename);
42              
43             #preserve mode, atime, and utime of old file
44 3 50       26 if ($PRESERVE_FILE_ATTRS) {
45 3         65 my @stat = stat $filename;
46              
47 3         48 utime( $stat[8], $stat[9], $newfile );
48 3         496 chmod $stat[2], $newfile;
49              
50             #preserve ownership if possible
51 3 50 33     526 chown $stat[4], $stat[5], $newfile
52             if ( $REAL_USER_ID == 0 || $REAL_USER_ID == $stat[4] );
53             }
54              
55 3         464 return $newfile;
56             }
57              
58             # Backup directory -- takes file name, optional compression level (2-9) and
59             # returns new archive file name
60             # This sub can DIE -- so use eval
61             sub _backupdir {
62 1     1   5 my ( $dir, $level ) = @_;
63              
64 1 0 33     7 $level = 5 if ( !defined($level) || $level < 2 || $level > 9 );
      33        
65              
66 1 50       16 croak "$dir is not a directory\n" unless ( -d $dir );
67              
68 1         3 my @files;
69 1         13 my $tar = Archive::Tar->new();
70              
71             # "promote" warnings from File::Find to errors
72 1     0   27 local $SIG{ __WARN__ } = sub { croak $_[0] };
  0         0  
73              
74             #recursivelly add files to tar
75             find(
76 4     4   209 { wanted => sub { push( @files, $_ ) },
77 1         165 no_chdir => 1
78             },
79             $dir
80             );
81              
82             #save archive
83              
84 1   33     358 my $tmpout = IO::File->new_tmpfile() || croak "Failed to create tmpfile\n";
85 1         10 binmode($tmpout);
86              
87 1         382 $tar->add_files(@files);
88 1         2083 $tar->write( $tmpout, $level );
89              
90             #backup file will have _yyyymmdd extention added to it
91 1         2058 return _backup( $dir, $tmpout );
92             }
93              
94             # Perform file backup if necessary
95             # Arguments: $filename -- file/dir to backup
96             # Returns backup file name
97             sub _backup {
98 4     4   13 my ( $filename, $fh ) = @_;
99              
100 4 100       11 my $input = $fh ? $fh : $filename;
101              
102 4         16 $filename =~ s/\/$//; #remove trailing slash from paths
103              
104 4 100       58 my $ext = -d $filename ? ".tar.gz" : "";
105 4         40 my $mtime = ( stat $filename )[9];
106 4         89 my ( $mday, $mon, $year ) = ( localtime($mtime) )[ 3 .. 5 ];
107              
108 4 50       21 if ( $BACKUPDIR ne '' ) {
109              
110             #backup in BUDIR directory relative to dirname
111 0         0 my ( $name, $path ) = fileparse($filename);
112              
113 0 0       0 my $budir
114             = $BACKUPDIR =~ /^\//
115             ? $BACKUPDIR
116             : File::Spec->catfile( $path, $BACKUPDIR );
117              
118             #try to create backup dir if it does not exist
119 0 0 0     0 croak("Failed to create backup dir $BACKUPDIR\n")
120             unless ( -d $budir || mkdir( $budir, 0750 ) );
121              
122 0         0 $filename = File::Spec->catfile( $budir, $name );
123             }
124              
125 4         8 my $newfile = q{}; # EMPTY_STR
126 4         7 my $basefile = q{};
127 4         8 my $lastbackup = q{};
128 4         8 my $count = 0;
129              
130 4         27 $newfile = $basefile
131             = sprintf( "%s_%d%02d%02d", $filename, $year + 1900, $mon + 1, $mday );
132              
133             #find next available backup -- keep appending _counter to
134             #basefile name until available extention is found
135              
136 4         126 for ( $count = 0; -e "$newfile$ext"; $count++ ) {
137 2         81 $newfile = $basefile . "_" . ( $count + 1 );
138             }
139              
140 4         12 $newfile = $basefile;
141              
142 4 100       11 if ($count) {
143              
144             # more then 1 backup exists -- last backup has
145             # count-1 extention (if count-1 == 0 -> exception: lastbackup=$basefile)
146 2 50       9 $lastbackup = $count - 1 > 0 ? "${basefile}_" . ( $count - 1 ) : $basefile;
147 2         7 $newfile .= "_$count";
148             }
149              
150 4 100       14 if ( $lastbackup ne '' ) {
151              
152             # last backup exists -- check if current file
153             # is different from backup
154 2 100       10 _file_diff( $input, "$lastbackup$ext" ) || return "$lastbackup$ext";
155             }
156              
157             #backup file
158 3 100       35 seek( $input, 0, 0 ) if ( ref($input) );
159 3 50       155 copy( $input, "$newfile$ext", 4096 ) || croak("$!\n");
160 3         1570 return "$newfile$ext";
161             }
162              
163             # return true if files are different
164             # f1, f2 can either be file names or open file handles (by ref)
165             # NOTE: modifies filehandle position to 0
166             sub _file_diff {
167 2     2   6 my ( $f1, $f2 ) = @_;
168              
169 2         4 my ( @files, $fh, $ref, $n1, $n2 );
170              
171 2         7 foreach ( $f1, $f2 ) {
172 4         405 $ref = ref($_);
173 4 50       34 $fh = $ref ? $_ : IO::File->new( $_, "r" )
    50          
174             or croak "Failed to create file: $!\n";
175              
176 4         567 push(
177             @files,
178             { fh => $fh,
179             ref => $ref,
180             size => ( $fh->stat() )[7]
181             }
182             );
183 4         77 seek( $fh, 0, 0 );
184             }
185              
186 2 100       158 return 1 unless ( $files[0]->{ size } == $files[1]->{ size } );
187              
188 1         4 my ( $buf1, $buf2 );
189 1         3 my $diff = 0;
190              
191 1   66     10 while ( !$diff
      66        
192             && ( $n1 = read( $files[0]->{ fh }, $buf1, 4096 ) )
193             && ( $n2 = read( $files[1]->{ fh }, $buf2, 4096 ) ) )
194             {
195 1 50 33     565 $diff = 1 if ( $n1 != $n2 || $buf1 ne $buf2 );
196             }
197              
198             #close/restore filehandles
199 1         86 foreach (@files) {
200 2 50       34 if ( $_->{ ref } ) {
201 0         0 seek( $_->{ fh }, 0, 0 );
202             }
203             else {
204 2         19 $_->{ fh }->close();
205             }
206             }
207              
208 1         27 return $diff;
209             }
210              
211             1; # End of Dev::Util::OS
212              
213             =pod
214              
215             =encoding utf-8
216              
217             =head1 NAME
218              
219             Dev::Util::Backup - Simple backup functions for files and dirs
220              
221             =head1 VERSION
222              
223             Version v2.19.35
224              
225             =head1 SYNOPSIS
226              
227             The backup function will make a copy of a file or dir with the date of the file appended.
228             It returns the name of the new file. Directories are backed up by C<tar> and C<gz>.
229              
230             use Dev::Util::Backup qw(backup);
231              
232             my $backup_file = backup('myfile');
233             say $backup_file;
234              
235             my $backup_dir = backup('mydir/');
236             say $backup_dir;
237              
238             Will produce:
239              
240             myfile_20251025
241             mydir_20251025.tar.gz
242              
243             If the file has changed, calling C<backup('myfile')> again will create C<myfile_20251025_1>.
244             Each time C<backup> is called the appended counter will increase by 1 if C<myfile> has
245             changed since the last time it was called.
246              
247             If the file has not changed, no new backup will be created.
248              
249             =head2 Examples
250              
251             The C<bu> program in the examples dir will take a list of files and dirs as args and make
252             backups of them using C<backup>.
253              
254             =head1 EXPORT
255              
256             backup
257              
258             =head1 SUBROUTINES
259              
260             =head2 B<backup(FILE|DIR)>
261              
262             Return the name of the backup file.
263              
264             my $backup_file = backup('myfile');
265             my $backup_dir = backup('mydir/');
266              
267              
268             =head1 AUTHOR
269              
270             Matt Martini, C<< <matt at imaginarywave.com> >>
271              
272             =head1 BUGS
273              
274             Please report any bugs or feature requests to C<bug-dev-util at rt.cpan.org>, or through
275             the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Dev-Util>. I will
276             be notified, and then you'll automatically be notified of progress on your bug as I make changes.
277              
278             =head1 SUPPORT
279              
280             You can find documentation for this module with the perldoc command.
281              
282             perldoc Dev::Util::Backup
283              
284             You can also look for information at:
285              
286             =over 4
287              
288             =item * RT: CPAN's request tracker (report bugs here)
289              
290             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Dev-Util>
291              
292             =item * Search CPAN
293              
294             L<https://metacpan.org/release/Dev-Util>
295              
296             =back
297              
298             =head1 ACKNOWLEDGMENTS
299              
300             =head1 LICENSE AND COPYRIGHT
301              
302             This software is Copyright © 2001-2025 by Matt Martini.
303              
304             This is free software, licensed under:
305              
306             The GNU General Public License, Version 3, June 2007
307              
308             =cut
309              
310             __END__