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   731225 use strict;
  13         19  
  13         338  
4 13     13   43 use warnings;
  13         13  
  13         245  
5 13     13   42 use Carp;
  13         13  
  13         682  
6              
7             our $VERSION = '0.2.2';
8              
9 13     13   45 use parent 'Exporter';
  13         16  
  13         61  
10 13     13   6245 use File::Copy ();
  13         22327  
  13         293  
11 13     13   2049 use File::Spec::Functions qw(splitpath catpath catfile);
  13         2671  
  13         777  
12 13     13   56 use File::Basename qw(basename dirname);
  13         14  
  13         453  
13 13     13   45 use Fcntl;
  13         17  
  13         6665  
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   84 my $pkg = shift;
25              
26 12         25 my %args = @_;
27              
28 12 100       53 if ( exists $args{-warn} ) {
29 1         2 $WarnNewFile = delete $args{-warn};
30             }
31              
32 12   66     72 $pattern = delete $args{-pattern} // $pattern;
33 12         34 _check_pattern($pattern);
34              
35 12         21 @_ = %args;
36              
37 12         60933 __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   38 my($from,$to) = @_;
54              
55 35         959 my $from_bn = basename $from;
56 35 100       617 my $orig_dest_file = my $dest_file = -d $to ? catfile( $to, $from_bn ) : $to;
57              
58 35         43 my $fh;
59              
60 35         38 my $write_test = 1;
61              
62 35 100       234 if ( -d $to ) {
    100          
63 32         191 $write_test = -w $to;
64             }
65             elsif ( -f $to ) {
66 1         27 $write_test = -w dirname $to;
67             }
68              
69 35 50       82 if ( not $write_test ) {
70 0         0 croak "Destination is not writable";
71             }
72              
73 35 100 66     377 if ( -f $from and ref $to ne "GLOB" ) {
74              
75 34 100       837 if ( !-d dirname $to ) {
76 1         22 croak "Invalid destination, should be in an existing directory";
77             }
78              
79             # use eval in case autodie or friends get in here
80 33         44 my $opened = eval {
81 33         1049 sysopen $fh, $dest_file, O_EXCL|O_CREAT|O_WRONLY;
82             };
83              
84 33         63 my $count = 0;
85 33         183 my $fp = filename_with_sprintf_pattern( $dest_file );
86              
87 33   66 10   305 while (not $opened and $!{EEXIST} ) {
  10         4288  
  10         8689  
  10         4000  
88              
89 18         244 $opened = eval {
90 18         838 sysopen
91             $fh,
92             ($dest_file = sprintf( $fp, ++$count )),
93             O_CREAT|O_EXCL|O_WRONLY;
94             };
95              
96 18 50       124 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       70 if (not fileno $fh) {
103 0         0 croak $!;
104             }
105              
106 33         60 binmode $fh;
107 33         62 switch_off_buffering($fh);
108              
109             }
110              
111 34 100 100     123 if ($dest_file ne $orig_dest_file and $WarnNewFile) {
112 1         70 carp "Destination changed to " . basename $dest_file;
113             }
114              
115 34         585 return ($fh,$dest_file);
116              
117             }
118              
119             sub copy {
120              
121 33     33 1 25484 my @args = @_;
122              
123 33         50 my($from,$to,$buffersize) = @args;
124              
125 33         68 my($fh,$dest_file) = _declobber($from,$to);
126              
127 32   66     83 $args[1] = $fh // $dest_file;
128              
129             # return destination filename, as it may be altered
130 32   100     90 return File::Copy::copy(@args) && $dest_file;
131              
132             }
133              
134             sub move {
135              
136 2     2 1 2396 my @args = @_;
137              
138 2         5 my($from,$to,$buffersize) = @args;
139              
140 2         7 my($fh,$dest_file) = _declobber($from,$to);
141 2         15 close $fh;
142              
143 2         4 $args[1] = $dest_file;
144              
145             # return destination filename, as it may be altered
146 2   66     7 return File::Copy::move(@args) && $dest_file;
147              
148             }
149              
150             sub filename_with_sprintf_pattern {
151              
152 35     35 0 1589 (my $path = shift) =~ s/%/%%/g;
153              
154 35         97 my($vol,$dir,$fn) = splitpath($path);
155              
156 35 100       404 if ( $fn =~ /\./ ) {
157 13 50       50 $fn =~
158              
159             s{ (?= \. [^\.]+ $ ) }
160 13         46 { $pattern }ex
161              
162             or die "Failed inserting noclobbering pattern into file";
163             }
164             else {
165 22         31 $fn .= $pattern;
166             }
167              
168 35         81 return catpath($vol,$dir,$fn);
169              
170             }
171              
172             sub switch_off_buffering {
173 33     33 0 77 my $h = select(shift);
174 33         57 $|=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.