File Coverage

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


line stmt bran cond sub pod time code
1             package File::Copy::NoClobber;
2              
3 13     13   1039015 use strict;
  13         20  
  13         285  
4 13     13   43 use warnings;
  13         17  
  13         236  
5 13     13   47 use Carp;
  13         12  
  13         672  
6              
7             our $VERSION = '0.2.3';
8              
9 13     13   46 use parent 'Exporter';
  13         11  
  13         60  
10 13     13   5974 use File::Copy ();
  13         21258  
  13         290  
11 13     13   2008 use File::Spec::Functions qw(splitpath catpath catfile);
  13         2644  
  13         677  
12 13     13   57 use File::Basename qw(basename dirname);
  13         13  
  13         434  
13 13     13   42 use Fcntl;
  13         13  
  13         6809  
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   83 my $pkg = shift;
25              
26 12         17 my %args = @_;
27              
28 12 100       57 if ( exists $args{-warn} ) {
29 1         2 $WarnNewFile = delete $args{-warn};
30             }
31              
32 12   66     63 $pattern = delete $args{-pattern} // $pattern;
33 12         33 _check_pattern($pattern);
34              
35 12         20 @_ = %args;
36              
37 12         72475 __PACKAGE__->export_to_level( 1, $pkg, @_ );
38              
39             }
40              
41             sub _check_pattern {
42              
43 12     12   14 my $ptn = shift;
44              
45 12 50       78 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         917 my $from_bn = basename $from;
56 35 100       544 my $orig_dest_file = my $dest_file = -d $to ? catfile( $to, $from_bn ) : $to;
57              
58 35         37 my $fh;
59              
60 35         32 my $write_test = 1;
61              
62 35 100       235 if ( -d $to ) {
    100          
63 32         178 $write_test = -w $to;
64             }
65             elsif ( -f $to ) {
66 1         36 $write_test = -w dirname $to;
67             }
68              
69 35 50       88 if ( not $write_test ) {
70 0         0 croak "Destination is not writable";
71             }
72              
73 35 100 66     364 if ( -f $from and ref $to ne "GLOB" ) {
74              
75 34 100       824 if ( !-d dirname $to ) {
76 1         34 croak "Invalid destination, should be in an existing directory";
77             }
78              
79             # use eval in case autodie or friends get in here
80 33         50 my $opened = eval {
81 33         1070 sysopen $fh, $dest_file, O_EXCL|O_CREAT|O_WRONLY;
82             };
83              
84 33         64 my $count = 0;
85 33         167 my $fp = filename_with_sprintf_pattern( $dest_file );
86              
87 33   66 10   298 while (not $opened and $!{EEXIST} ) {
  10         4050  
  10         8624  
  10         3912  
88              
89 18         315 $opened = eval {
90 18         797 sysopen
91             $fh,
92             ($dest_file = sprintf( $fp, ++$count )),
93             O_CREAT|O_EXCL|O_WRONLY;
94             };
95              
96 18 50       78 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       76 if (not fileno $fh) {
103 0         0 croak $!;
104             }
105              
106 33         55 binmode $fh;
107 33         53 switch_off_buffering($fh);
108              
109             }
110              
111 34 100 100     144 if ($dest_file ne $orig_dest_file and $WarnNewFile) {
112 1         44 carp "Destination changed to " . basename $dest_file;
113             }
114              
115 34         502 return ($fh,$dest_file);
116              
117             }
118              
119             sub copy {
120              
121 33     33 1 26414 my @args = @_;
122              
123 33         108 my($from,$to,$buffersize) = @args;
124              
125 33         67 my($fh,$dest_file) = _declobber($from,$to);
126              
127 32   66     67 $args[1] = $fh // $dest_file;
128              
129             # return destination filename, as it may be altered
130 32   100     88 return File::Copy::copy(@args) && $dest_file;
131              
132             }
133              
134             sub move {
135              
136 2     2 1 2135 my @args = @_;
137              
138 2         4 my($from,$to,$buffersize) = @args;
139              
140 2         7 my($fh,$dest_file) = _declobber($from,$to);
141 2         12 close $fh;
142              
143 2         4 $args[1] = $dest_file;
144              
145             # return destination filename, as it may be altered
146 2   33     9 return File::Copy::move(@args) && $dest_file;
147              
148             }
149              
150             sub filename_with_sprintf_pattern {
151              
152 35     35 0 1972 (my $path = shift) =~ s/%/%%/g;
153              
154 35         100 my($vol,$dir,$fn) = splitpath($path);
155              
156 35 100       388 if ( $fn =~ /\./ ) {
157 13 50       51 $fn =~
158              
159             s{ (?= \. [^\.]+ $ ) }
160 13         46 { $pattern }ex
161              
162             or die "Failed inserting noclobbering pattern into file";
163             }
164             else {
165 22         27 $fn .= $pattern;
166             }
167              
168 35         95 return catpath($vol,$dir,$fn);
169              
170             }
171              
172             sub switch_off_buffering {
173 33     33 0 79 my $h = select(shift);
174 33         54 $|=1;
175 33         77 select($h);
176             }
177              
178             1;
179              
180             =encoding utf8
181              
182             =head1 NAME
183              
184             File::Copy::NoClobber - Rename copied files safely if destination 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             Supports the same arguments as L.
210              
211             Checks if the operation would overwrite an existing file, if so adds a
212             counter to the destination 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 unsuccessful.
222              
223             =head2 move( $from, $to )
224              
225             Supports 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 LICENSE 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.