File Coverage

blib/lib/IO/BlockSync.pm
Criterion Covered Total %
statement 38 122 31.1
branch 0 42 0.0
condition 0 27 0.0
subroutine 13 19 68.4
pod 2 2 100.0
total 53 212 25.0


line stmt bran cond sub pod time code
1             package IO::BlockSync;
2              
3             # Basic
4 1     1   64680 use 5.010;
  1         4  
5 1     1   5 use strict;
  1         2  
  1         36  
6 1     1   7 use warnings FATAL => 'all';
  1         2  
  1         40  
7              
8             # Build in
9 1     1   5 use Carp;
  1         2  
  1         70  
10 1     1   6 use Fcntl qw(:DEFAULT :seek);
  1         2  
  1         425  
11 1     1   562 use POSIX qw(ceil);
  1         6187  
  1         6  
12 1     1   1435 use Scalar::Util qw(reftype);
  1         2  
  1         59  
13              
14             # CPAN
15 1     1   826 use Log::Log4perl;
  1         45528  
  1         4  
16 1     1   59 use Log::Log4perl::Level;
  1         2  
  1         5  
17 1     1   699 use Moo;
  1         11507  
  1         6  
18 1     1   2043 use Try::Tiny;
  1         1269  
  1         56  
19              
20             # These two come last - in that order
21 1     1   461 use namespace::clean;
  1         9241  
  1         6  
22 1     1   1321 use Exporter qw(import);
  1         3  
  1         1454  
23              
24             ################################################################
25              
26             # Moo roles to implement
27             with('MooseX::Log::Log4perl');
28              
29             # Make sure log4perl doesn't come with errors
30             if ( not Log::Log4perl->initialized() ) {
31             Log::Log4perl->easy_init( Log::Log4perl::Level::to_priority('OFF') );
32             }
33              
34             ################################################################
35              
36             =head1 NAME
37              
38             IO::BlockSync - Syncronize changed blocks
39              
40             =head1 VERSION
41              
42             Version 0.002
43              
44             =cut
45              
46             our $VERSION = '0.002';
47              
48             ################################################################
49              
50             =head1 SYNOPSIS
51              
52             BlockSync can some of the same stuff that bigsync (by Egor Egorov) can
53             - it's just written in perl.
54              
55             BlockSync copies data from source file to destination file (can be a block
56             device) and calculates checksum on each block it copies.
57             On all runs after the first only the changed blocks will be copied.
58              
59             use IO::BlockSync;
60              
61             # OOP way
62             my $bs = IO::BlockSync->new(
63             src => '/path/to/source/file',
64             dst => '/path/to/destination/file',
65             chk => '/path/to/chk/file',
66             );
67             $bs->run;
68              
69             # Non OOP way
70             BlockSync(
71             src => '/path/to/source/file',
72             dst => '/path/to/destination/file',
73             chk => '/path/to/chk/file',
74             );
75              
76             =cut
77              
78             ################################################################
79              
80             =head1 INSTALLATION
81              
82             Look in C<README.pod>
83              
84             Can also be found on
85             L<GitHub|https://github.com/thordreier/perl-IO-BlockSync/blob/master/README.pod>
86             or L<meta::cpan|https://metacpan.org/pod/distribution/IO-BlockSync/README.pod>
87              
88             =cut
89              
90             ################################################################
91              
92             =head1 EXPORT
93              
94             =cut
95              
96             our @EXPORT = qw(BlockSync);
97              
98             ################################################################
99              
100             =head2 BlockSync
101              
102             Run BlockSync non-object-oriented
103              
104             =cut
105              
106             sub BlockSync {
107 0     0 1   return __PACKAGE__->new(@_)->run;
108             }
109              
110             ################################################################
111              
112             =head1 ATTRIBUTES
113              
114             =cut
115              
116             ################################################################
117              
118             =head2 src
119              
120             Path to source file.
121              
122             mandatory - string (containing path) or filehandle
123              
124             =cut
125              
126             has 'src' => (
127             is => 'ro',
128             required => 1,
129             );
130              
131             ################################################################
132              
133             =head2 dst
134              
135             Destination file. If not set, then only checksum file will be updated.
136              
137             optional - string (containing path) or filehandle
138              
139             =cut
140              
141             has 'dst' => ( is => 'ro', );
142              
143             ################################################################
144              
145             =head2 chk
146              
147             Path to checksum file.
148              
149             mandatory - string (containing path) or filehandle
150              
151             =cut
152              
153             has 'chk' => (
154             is => 'ro',
155             required => 1,
156             );
157              
158             ################################################################
159              
160             =head2 bs
161              
162             Block size to use in bytes.
163              
164             optional - integer - defaults to 1_048_576 B (1 MB)
165              
166             =cut
167              
168             has 'bs' => (
169             is => 'ro',
170             default => 1_048_576,
171             );
172              
173             ################################################################
174              
175             =head2 hash
176              
177             Sub that retrurn hashed data.
178              
179             optional - sub - defaults to sub that return MD5 hash followed by newline
180              
181             =cut
182              
183             has 'hash' => (
184             is => 'ro',
185             default => sub {
186             require Digest::MD5;
187             sub {
188             Digest::MD5::md5_hex(shift) . "\n";
189             }
190             },
191             );
192              
193             ################################################################
194              
195             =head2 sparse
196              
197             Seek in dst file, instead of writing blocks only containing \0
198              
199             optional - boolean - defaults to 0 (false)
200              
201             =cut
202              
203             has 'sparse' => (
204             is => 'ro',
205             default => 0,
206             );
207              
208             ################################################################
209              
210             =head2 truncate
211              
212             Truncate the destination file to same size as source file. Does not work on block devices. Will only be tried if C<data> has default value (whole file is copied).
213              
214             optional - boolean - defaults to 0 (false)
215              
216             =cut
217              
218             has 'truncate' => (
219             is => 'ro',
220             default => 0,
221             );
222              
223             ################################################################
224              
225             =head2 data
226              
227             List of areas (in bytes) inside the source file that should be looked at.
228             Usefull if you know excactly which blocks in src that could have changed.
229              
230             data => [
231             {start => 0, end => 9999},
232             {start => 88888, end => 777777},
233             ]
234              
235             optional - array of hashes - defaults to "whole file"
236              
237             =cut
238              
239             has 'data' => (
240             is => 'ro',
241             default => sub {
242             [
243             {
244             start => 0,
245             end => 0
246             }
247             ]
248             },
249             );
250              
251             ################################################################
252              
253             =head2 status
254              
255             Sub that will be run everytime a block has been read (and written).
256              
257             optional - sub - default to sub doing nothing
258              
259             =cut
260              
261             has 'status' => (
262             is => 'ro',
263             default => sub {
264             sub { }
265             },
266             );
267              
268             ################################################################
269              
270             =head1 METHODS
271              
272             =cut
273              
274             ################################################################
275              
276             =head2 run
277              
278             This is the method that starts copying data.
279              
280             =cut
281              
282             sub run { ## no critic (Subroutines::ProhibitExcessComplexity)
283 0     0 1   my $self = shift;
284              
285 0           my ( $srcFh, $srcClose, $dstFh, $dstClose, $chkFh, $chkClose );
286              
287             try {
288             # Get file handles for source, destination and checksum files
289 0     0     $srcFh = $self->_getFh( 'src', $self->src, \$srcClose, O_RDONLY );
290 0           $chkFh =
291             $self->_getFh( 'chk', $self->chk, \$chkClose, O_RDWR | O_CREAT );
292 0 0         if ( $self->dst ) {
293 0           $dstFh = $self->_getFh( 'dst', $self->dst, \$dstClose,
294             O_WRONLY | O_CREAT );
295             }
296             else {
297 0           $self->logger->debug('No dst file, only calculating checksums');
298             }
299              
300             # Calculate hash for a block only containing ASCII 0
301 0           my $nullHash = &{ $self->hash }( "\0" x $self->bs );
  0            
302              
303             # Get number of bytes that a hash takes up
304 0           my $hashSize = length($nullHash);
305              
306 0           my $srcSeek;
307              
308             # Loop through "areas" that should be copied
309             # Default i one area containing the whole source file
310 0           foreach my $dataBlocks ( @{ $self->data } ) {
  0            
311              
312             # Start and end of this "area" (default is $start=0, $end=0)
313 0           my $start = $dataBlocks->{start};
314 0           my $end = $dataBlocks->{end};
315              
316 0           $self->logger->debug(
317             "Going to process data from <$start> to <$end>");
318              
319             # Seek to $start
320             # (or the beginning of the block that $start is in,
321             # if $start is not aligned with bs)
322 0           $srcSeek = int( $start / $self->bs ) * $self->bs;
323 0 0         sysseek( $srcFh, $srcSeek, SEEK_SET )
324             || $self->logger->logcroak(
325             "Cannot seek to block <$srcSeek> in src file");
326              
327             # Just die! Muhahaha. Or not
328 0           my $die = 0;
329              
330             # Can be either sparse, new, unchanged or changed
331 0           my $status;
332              
333             # Read block from source
334 0           while ( my $srcReadSize = sysread( $srcFh, my $data, $self->bs ) ) {
335              
336             # It's ok to read a block smaller than bs if it's the last
337             # block. But it's not ok if it's not the last.
338 0 0         if ($die) {
339 0           croak 'not reading full block';
340             }
341 0           $die = $srcReadSize != $self->bs;
342              
343             # $block = block number in source with the specified block size
344 0           my $block = $srcSeek / $self->bs;
345              
346             # We start by assuming that we should write to dst
347             # - if dst is set (= we are not just calculating checksum)
348 0           my $writeData = 1 && $dstFh;
349              
350             # We start be assuming that we should write checksum to chk
351 0           my $writeHash = 1;
352              
353             # Calculate hash for data read from src
354 0           my $newHash = &{ $self->hash }($data);
  0            
355              
356             # Get old hash for the same block
357 0 0         sysseek( $chkFh, $block * $hashSize, SEEK_SET )
358             || $self->logger->logcroak('Cannot seek in chk file');
359 0           my $oldHashSize = sysread( $chkFh, my $oldHash, $hashSize );
360              
361             # Test source against checksum
362 0 0 0       if ( $oldHashSize != $hashSize || $oldHash eq "\0" x $hashSize )
    0          
363             {
364 0 0 0       if ( $self->sparse && $newHash eq $nullHash ) {
365              
366             # Sparse is only for new blocks
367             # Blocks that have been nulled out in source will
368             # also get nulled out in destination
369 0           $status = 'sparse';
370 0           $writeData = 0;
371             }
372             else {
373 0           $status = 'new';
374             }
375             }
376             elsif ( $newHash eq $oldHash ) {
377 0           $status = 'unchanged';
378 0           $writeData = 0;
379 0           $writeHash = 0;
380             }
381             else {
382 0           $status = 'changed';
383             }
384              
385             # Write data to destination
386 0 0         if ($writeData) {
387 0 0         sysseek( $dstFh, $srcSeek, SEEK_SET )
388             || $self->logger->logcroak('Cannot seek in dst file');
389 0           syswrite( $dstFh, $data );
390             }
391              
392             # Update hash in checksum
393 0 0         if ($writeHash) {
394 0 0         sysseek( $chkFh, $block * $hashSize, SEEK_SET )
395             || $self->logger->logcroak('Cannot seek in chk file');
396 0           syswrite( $chkFh, $newHash );
397             }
398              
399             $self->logger->debug(
400 0           sprintf 'Block <%u> was <%s> (<%u> to <%u>)',
401             $block, $status, $srcSeek, $srcSeek + $srcReadSize - 1 );
402 0           &{ $self->status }
  0            
403             ( $block, $status, $srcSeek, $srcSeek + $srcReadSize - 1 );
404              
405             # Next block will start here
406 0           $srcSeek += $srcReadSize;
407              
408             # Was this the last block in this batch
409 0 0 0       if ( $end && $srcSeek > $end ) {
410 0           last;
411             }
412             }
413              
414             # If last block is sparse, it is not enough to seek to where the
415             # EOF should be. We need to at least write a single \0
416 0 0 0       if ( $dstFh && $status eq 'sparse' ) {
417 0 0         sysseek( $dstFh, $srcSeek - 1, SEEK_SET )
418             || $self->logger->logcroak('Cannot seek in dst file');
419 0           syswrite( $dstFh, "\0" );
420             }
421             }
422              
423 0 0 0       if ( $self->truncate
      0        
      0        
      0        
      0        
424             && $dstFh
425             && $srcSeek
426 0           && @{ $self->data } == 1
427             && $self->data->[0]->{start} == 0
428             && $self->data->[0]->{end} == 0 )
429             {
430 0           $self->logger->debug("Truncating dst file at <$srcSeek>");
431 0           truncate( $dstFh, $srcSeek );
432              
433 0           my $chkSeek = ceil( $srcSeek / $self->bs ) * $hashSize;
434 0           $self->logger->debug("Truncating chk file at <$chkSeek>");
435 0           truncate( $chkFh, $chkSeek );
436             }
437              
438             }
439             catch {
440 0     0     croak $_;
441             }
442             finally {
443             # If we opened the files (we got string with path), then we close them
444             # if we got a filehandle then we do nothing
445 0 0   0     if ($srcClose) {
446 0           $self->logger->debug( sprintf 'closing src file <%s>', $self->src );
447             }
448 0 0         if ($dstClose) {
449 0           $self->logger->debug( sprintf 'closing dst file <%s>', $self->dst );
450             }
451 0 0         if ($chkClose) {
452 0           $self->logger->debug( sprintf 'closing chk file <%s>', $self->chk );
453             }
454 0           };
455              
456             # Make Perl::Critic happy
457 0           return;
458             }
459              
460             ################################################################
461              
462             =begin comment
463              
464             Private
465             Get file handle
466              
467             =end comment
468              
469             =cut
470              
471             sub _getFh {
472 0     0     my ( $self, $name, $file, $closeFile, $mode ) = @_;
473              
474 0 0         if ( my $t = reftype($file) ) {
475 0 0         if ( $t eq 'GLOB' ) {
476 0           $self->logger->debug(
477             sprintf '%s is a file handle, using that directly', $name );
478 0           return $file;
479             }
480             else {
481 0           $self->logger->logcroak(
482             sprintf '<%s> is not a supported type for %s',
483             $t, $name );
484             }
485             }
486             else {
487 0           $self->logger->debug( sprintf 'opening %s file <%s>', $name, $file );
488 0 0         sysopen( my $fh, $file, $mode )
489             || $self->logger->logcroak( sprintf 'error opening <%s>', $file );
490 0           ${$closeFile} = 1;
  0            
491 0           return $fh;
492             }
493              
494             # Make Perl::Critic happy
495 0           croak 'We should never end here!';
496             }
497              
498             ################################################################
499              
500             =head1 LICENSE AND COPYRIGHT
501              
502             This software is copyright (c) 2019 by Thor Dreier-Hansen.
503              
504             This is free software; you can redistribute it and/or modify it under
505             the same terms as the Perl 5 programming language system itself.
506              
507             Terms of the Perl programming language system itself:
508              
509             =over
510              
511             =item * the
512             L<GNU General Public License|http://dev.perl.org/licenses/gpl1.html>
513             as published by the Free Software Foundation; either
514             L<version 1|http://dev.perl.org/licenses/gpl1.html>,
515             or (at your option) any later version, or
516              
517             =item * the L<"Artistic License"|http://dev.perl.org/licenses/artistic.html>
518              
519             =back
520              
521             See L<http://dev.perl.org/licenses/> for more information.
522              
523             =cut
524              
525             1; # End of IO::BlockSync