File Coverage

blib/lib/File/Copy/NoClobber.pm
Criterion Covered Total %
statement 84 88 95.4
branch 21 26 80.7
condition 16 21 76.1
subroutine 16 16 100.0
pod 2 4 50.0
total 139 155 89.6


line stmt bran cond sub pod time code
1             package File::Copy::NoClobber;
2              
3 13     13   712459 use strict;
  13         17  
  13         309  
4 13     13   40 use warnings;
  13         12  
  13         221  
5 13     13   45 use Carp;
  13         9  
  13         634  
6              
7             our $VERSION = '0.2.1';
8              
9 13     13   48 use parent 'Exporter';
  13         20  
  13         62  
10 13     13   6268 use File::Copy ();
  13         20996  
  13         288  
11 13     13   2200 use File::Spec::Functions qw(splitpath catpath catfile);
  13         2699  
  13         710  
12 13     13   53 use File::Basename qw(basename dirname);
  13         13  
  13         438  
13 13     13   41 use Fcntl;
  13         17  
  13         6789  
14              
15             our @EXPORT = qw(copy move);
16              
17             my $pattern = " (%02d)";
18             my $MAX_COUNT = 1e4;
19              
20             my $WarnNewFile = 0;
21              
22             sub import {
23              
24 12     12   75 my $pkg = shift;
25              
26 12         21 my %args = @_;
27              
28 12 100       52 if ( exists $args{-warn} ) {
29 1         3 $WarnNewFile = delete $args{-warn};
30             }
31              
32 12   66     60 $pattern = delete $args{-pattern} // $pattern;
33 12         31 _check_pattern($pattern);
34              
35 12         22 @_ = %args;
36              
37 12         65764 __PACKAGE__->export_to_level( 1, $pkg, @_ );
38              
39             }
40              
41             sub _check_pattern {
42              
43 12     12   16 my $ptn = shift;
44              
45 12 50       75 if ( sprintf($ptn,1) eq sprintf($ptn,2) ) {
46 0         0 croak "Invalid noclobber pattern '$pattern'";
47             }
48              
49             }
50              
51             sub _declobber {
52              
53 35     35   37 my($from,$to) = @_;
54              
55 35         932 my $from_bn = basename $from;
56 35 100       563 my $orig_dest_file = my $dest_file = -d $to ? catfile( $to, $from_bn ) : $to;
57              
58 35         36 my $fh;
59              
60 35         41 my $write_test = 1;
61              
62 35 100       232 if ( -d $to ) {
    100          
63 32         200 $write_test = -w $to;
64             }
65             elsif ( -f $to ) {
66 1         27 $write_test = -w dirname $to;
67             }
68              
69 35 50       85 if ( not $write_test ) {
70 0         0 croak "Destination is not writable";
71             }
72              
73 35 100 66     375 if ( -f $from and ref $to ne "GLOB" ) {
74              
75 34 100       820 if ( !-d dirname $to ) {
76 1         20 croak "Invalid destination, should be in an existing directory";
77             }
78              
79             # use eval in case autodie or friends get in here
80 33         43 my $opened = eval {
81 33         1037 sysopen $fh, $dest_file, O_EXCL|O_CREAT|O_WRONLY;
82             };
83              
84 33         88 my $count = 0;
85 33         177 my $fp = filename_with_sprintf_pattern( $dest_file );
86              
87 33   66 10   296 while (not $opened and $!{EEXIST} ) {
  10         4001  
  10         8676  
  10         3758  
88              
89 18         251 $opened = eval {
90 18         816 sysopen
91             $fh,
92             ($dest_file = sprintf( $fp, ++$count )),
93             O_CREAT|O_EXCL|O_WRONLY;
94             };
95              
96 18 50       75 if ($count > $MAX_COUNT) {
97 0         0 croak "Failed to find a nonclobbering filename, tried to increment counter $MAX_COUNT times";
98             }
99              
100             }
101              
102 33 50       68 if (not fileno $fh) {
103 0         0 croak $!;
104             }
105              
106 33         60 binmode $fh;
107 33         56 switch_off_buffering($fh);
108              
109             }
110              
111 34 100 100     129 if ($dest_file ne $orig_dest_file and $WarnNewFile) {
112 1         46 carp "Destionation changed to " . basename $dest_file;
113             }
114              
115 34         618 return ($fh,$dest_file);
116              
117             }
118              
119             sub copy {
120              
121 33     33 1 34604 my @args = @_;
122              
123 33         52 my($from,$to,$buffersize) = @args;
124              
125 33         65 my($fh,$dest_file) = _declobber($from,$to);
126              
127 32   66     81 $args[1] = $fh // $dest_file;
128              
129             # return destination filename, as it may be altered
130 32   100     96 return File::Copy::copy(@args) && $dest_file;
131              
132             }
133              
134             sub move {
135              
136 2     2 1 2212 my @args = @_;
137              
138 2         4 my($from,$to,$buffersize) = @args;
139              
140 2         6 my($fh,$dest_file) = _declobber($from,$to);
141 2         15 close $fh;
142              
143 2         3 $args[1] = $dest_file;
144              
145             # return destination filename, as it may be altered
146 2   66     6 return File::Copy::move(@args) && $dest_file;
147              
148             }
149              
150             sub filename_with_sprintf_pattern {
151              
152 35     35 0 1559 (my $path = shift) =~ s/%/%%/g;
153              
154 35         88 my($vol,$dir,$fn) = splitpath($path);
155              
156 35 100       390 if ( $fn =~ /\./ ) {
157 13 50       50 $fn =~
158              
159             s{ (?= \. [^\.]+ $ ) }
160 13         49 { $pattern }ex
161              
162             or die "Failed inserting noclobbering pattern into file";
163             }
164             else {
165 22         26 $fn .= $pattern;
166             }
167              
168 35         78 return catpath($vol,$dir,$fn);
169              
170             }
171              
172             sub switch_off_buffering {
173 33     33 0 72 my $h = select(shift);
174 33         61 $|=1;
175 33         72 select($h);
176             }
177              
178             1;
179              
180             =encoding utf8
181              
182             =head1 NAME
183              
184             File::Copy::NoClobber - Rename copied files safely if destionation exists
185              
186             =head1 SYNOPSIS
187              
188             use File::Copy::NoClobber;
189              
190             copy( "file.txt", "elsewhere/" ); # elsewhere/file.txt
191             copy( "file.txt", "elsewhere/" ); # elsewhere/file (01).txt
192              
193             # similar with move
194             move( "file.txt", "elsewhere/" ); # elsewhere/file (02).txt
195              
196             use File::Copy::NoClobber -warn => 1; # warns when name is changed
197              
198             use File::Copy::NoClobber -pattern => "[%04d]" # custom noclobber
199              
200             =head1 DESCRIPTION
201              
202             The module exports copy() and move(). They are wrappers around C
203             and C in L.
204              
205             =head1 INTERFACE
206              
207             =head2 copy( $from, $to [, $buffersize] )
208              
209             Supportes the same arguments as L.
210              
211             Checks if the operation would overwrite an existing file, if so adds a
212             counter to the destionation filename as shown in the SYNOPSIS.
213              
214             The module uses sysopen with O_EXCL and an increasing counter to
215             determine a working filename. The second argument is then replaced
216             with this filehandle and passed to C.
217              
218             The counter inserted to filenames is C<" (%02d)"> by default, but can
219             be changed on import.
220              
221             It returns the filename written to or undef if unsuccesful.
222              
223             =head2 move( $from, $to )
224              
225             Supportes the same arguments as L.
226              
227             Determines destination filename in the same way as C, but the
228             move operation is used on the filename rather than the filehandle, to
229             allow rename to be used.
230              
231             =head1 DEPENDENCIES
232              
233             This module does not introduce dependencies. It does not use modules
234             not already in use in File::Copy.
235              
236             =head1 AUTHOR
237              
238             Torbjørn Lindahl C<< torbjorn.lindahl@gmail.com >>
239              
240             =head1 CONTRIBUTORS
241              
242             Core ideas from I, I and I in #perl@freenode
243              
244             =head1 LICENCE AND COPYRIGHT
245              
246             Copyright (c) 2016, Torbjørn Lindahl C<< torbjorn.lindahl@gmail.com >>.
247             All rights reserved.
248              
249             This module is free software; you can redistribute it and/or modify it
250             under the same terms as Perl itself. See L.