File Coverage

blib/lib/Asset/File.pm
Criterion Covered Total %
statement 115 138 83.3
branch 27 42 64.2
condition 13 19 68.4
subroutine 24 26 92.3
pod 10 13 76.9
total 189 238 79.4


line stmt bran cond sub pod time code
1             package Asset::File;
2 1     1   18502 use Moo;
  1         11814  
  1         4  
3 1     1   1218 use Carp 'croak';
  1         2  
  1         62  
4 1     1   453 use Errno 'EEXIST';
  1         1018  
  1         102  
5 1     1   5 use Fcntl qw(O_APPEND O_CREAT O_EXCL O_RDONLY O_RDWR);
  1         1  
  1         53  
6 1     1   471 use File::Copy 'move';
  1         25043  
  1         77  
7 1     1   7 use File::Spec::Functions 'catfile';
  1         1  
  1         44  
8 1     1   755 use File::Temp;
  1         15474  
  1         72  
9 1     1   8 use File::Path;
  1         1  
  1         36  
10 1     1   6 use File::Basename;
  1         1  
  1         58  
11 1     1   4 use Digest::MD5 'md5_hex';
  1         2  
  1         41  
12 1     1   477 use Digest::SHA1;
  1         1740  
  1         57  
13 1     1   643 use IO::File;
  1         913  
  1         1500  
14             our $VERSION = '1.03';
15              
16             has [qw/cleanup path end_range ro/] => (
17             is => 'rw',
18             );
19              
20             has start_range => (
21             is => 'rw',
22             default => sub { 0 },
23             );
24              
25             has handle => (
26             is => 'rw',
27             lazy => 1,
28             default => sub {
29             my $self = shift;
30              
31             # Open existing file
32             my $handle = IO::File->new;
33             my $path = $self->path;
34             if (defined $path && -f $path) {
35             $handle->open($path, -w $path ? ($self->ro ? O_RDONLY : O_RDWR) : O_RDONLY)
36             or croak qq{Can't open file "$path": $!};
37             return $handle;
38             }
39              
40             # Open new or temporary file
41             my $out = File::Temp->new(UNLINK => $self->cleanup);
42             $out->autoflush(1);
43             my $base = $out->filename;
44             my $name = $path // $base;
45             until ($handle->open($name, O_CREAT | O_RDWR)) {
46             croak qq{Can't open file "$name": $!} if defined $path || $! != $!{EEXIST};
47             $name = "$base." . md5_hex(time . $$ . rand 999);
48             }
49             $self->path($name);
50              
51             # Enable automatic cleanup
52             $self->cleanup(1) unless defined $self->cleanup;
53              
54             return $handle;
55             }
56             );
57              
58              
59             sub DESTROY {
60 15     15   2453 my $self = shift;
61 15 100 66     329 return unless $self->cleanup && defined(my $path = $self->path);
62 9         297 close $self->handle;
63 9 50       1671 unlink $path if -w $path;
64             }
65              
66 3   100 3 0 44 sub is_range { !!($_[0]->end_range || $_[0]->start_range) };
67              
68             sub add_chunk {
69 19     19 1 3102 my ($self, $chunk) = @_;
70 19   50     62 $chunk //= '';
71 19         627 my $handle = $self->handle;
72 19 100       647 if ($self->start_range) {
73 1         8 $handle->sysseek($self->start_range, SEEK_SET);
74             }
75             else {
76 18         81 $handle->sysseek(0, SEEK_END);
77             }
78 19 50       198 croak "Can't write to asset: $!"
79             unless defined $handle->syswrite($chunk, length $chunk);
80 19         2121 return $self;
81             }
82              
83             sub contains {
84 37     37 1 795 my ($self, $str) = @_;
85              
86 37         1311 my $handle = $self->handle;
87 37         448 $handle->sysseek($self->start_range, SEEK_SET);
88              
89             # Calculate window size
90 37   100     384 my $end = $self->end_range // $self->size;
91 37         465 my $len = length $str;
92 37 100       87 my $size = $len > 131072 ? $len : 131072;
93 37 100       147 $size = $end - $self->start_range if $size > $end - $self->start_range;
94              
95             # Sliding window search
96 37         42 my $offset = 0;
97 37         126 my $start = $handle->sysread(my $window, $len);
98 37         609 while ($offset < $end) {
99              
100             # Read as much as possible
101 55         82 my $diff = $end - ($start + $offset);
102 55 100       206 my $read = $handle->sysread(my $buffer, $diff < $size ? $diff : $size);
103 55         2860 $window .= $buffer;
104              
105             # Search window
106 55         5143 my $pos = index $window, $str;
107 55 100       446 return $offset + $pos if $pos >= 0;
108 26 100 66     208 return -1 if $read == 0 || ($offset += $read) == $end;
109              
110             # Resize window
111 19         576 substr $window, 0, $read, '';
112             }
113              
114 1         7 return -1;
115             }
116              
117             sub get_chunk {
118 8     8 1 19 my ($self, $offset, $max) = @_;
119 8   100     30 $max //= 131072;
120              
121 8         22 $offset += $self->start_range;
122 8         264 my $handle = $self->handle;
123 8         75 $handle->sysseek($offset, SEEK_SET);
124              
125 8         52 my $buffer;
126 8 100       41 if (defined(my $end = $self->end_range)) {
127 7 50       22 return '' if (my $chunk = $end + 1 - $offset) <= 0;
128 7 100       37 $handle->sysread($buffer, $chunk > $max ? $max : $chunk);
129             }
130 1         5 else { $handle->sysread($buffer, $max) }
131              
132 8         135 return $buffer;
133             }
134              
135             sub first_line_of {
136 0     0 0 0 my $fh = shift->handle;
137 0         0 my $line = <$fh>;
138 0         0 chomp $line;
139 0         0 $line =~ s/^\s+|\s+$//g;
140              
141 0         0 return $line;
142             }
143              
144             sub md5sum {
145 1     1 1 9 my $self = shift;
146 1         2 my $content = shift;
147 1         13 my $md5 = Digest::MD5->new;
148 1 50       5 if ($content) {
149 0         0 $md5->add($content);
150 0         0 return $md5->hexdigest,
151             }
152 1         37 my $handle = $self->handle;
153 1         12 $handle->sysseek(0, SEEK_SET);
154 1         10 while ($handle->sysread(my $buffer, 131072, 0)) {
155 1         30 $md5->add($buffer);
156             }
157 1         26 return $md5->hexdigest,
158             }
159              
160             sub sha1sum {
161 1     1 1 6 my $self = shift;
162 1         2 my $content = shift;
163 1         12 my $sha1 = Digest::SHA1->new;
164 1 50       5 if ($content) {
165 0         0 $sha1->add($content);
166 0         0 return $sha1->hexdigest,
167             }
168 1         35 my $handle = $self->handle;
169 1         12 $handle->sysseek(0, SEEK_SET);
170 1         10 while ($handle->sysread(my $buffer, 131072, 0)) {
171 1         39 $sha1->add($buffer);
172             }
173 1         27 return $sha1->hexdigest,
174             }
175              
176             sub crc32 {
177 0     0 0 0 my $self = shift;
178 0         0 my $content = shift;
179 0 0       0 eval q{ require Digest::CRC } or die 'Could not require Digest::CRC';
180 0         0 my $crc = Digest::CRC->new( type => "crc32" );
181 0         0 my $handle = $self->handle;
182 0         0 $handle->sysseek(0, SEEK_SET);
183 0 0       0 if ($content) {
184 0         0 $crc->add($content);
185 0         0 return $crc->hexdigest,
186             }
187 0         0 while ($handle->sysread(my $buffer, 131072, 0)) {
188 0         0 $crc->add($buffer);
189             }
190 0         0 return $crc->hexdigest,
191             }
192              
193 1     1 1 10 sub is_file {1}
194              
195             sub move_to {
196 2     2 1 12 my ($self, $to) = @_;
197              
198             # Windows requires that the handle is closed
199 2         62 close $self->handle;
200 2         81 delete $self->{handle};
201              
202 2         115 my $dir = File::Basename::dirname( $to );
203 2 50       46 if (! -e $dir ) {
204 0 0 0     0 if (! File::Path::make_path( $dir ) || ! -d $dir ) {
205 0         0 my $e = $!;
206             }
207             }
208              
209             # Move file and prevent clean up
210 2         9 my $from = $self->path;
211 2 50       11 move($from, $to) or croak qq{Can't move file "$from" to "$to": $!};
212 2         215 $self->cleanup(0);
213 2         10 $self->path($to);
214 2         10 return $self;
215             }
216              
217 3     3 1 1851 sub mtime { (stat shift->handle)[9] }
218              
219 27     27 1 760 sub size { -s shift->handle }
220              
221             sub slurp {
222 5 50   5 1 1049 return '' unless defined (my $path = shift->path);
223 5 50       236 croak qq{Can't open file "$path": $!} unless open my $file, '<', $path;
224 5         19 my $content = '';
225 5         35 while ($file->sysread(my $buffer, 131072, 0)) { $content .= $buffer }
  3         53  
226 5         151 return $content;
227             }
228              
229             1;
230              
231             =encoding utf8
232              
233             =head1 NAME
234              
235             Asset::File - File Operation interface
236              
237             =head1 SYNOPSIS
238              
239             use Asset::File;
240            
241             # Temporary file
242             my $file = Asset::File->new;
243             $file->add_chunk('foo bar baz');
244             say 'File contains "bar"' if $file->contains('bar') >= 0;
245             say $file->slurp;
246            
247             # Existing file
248             my $file = Asset::File->new(path => '/home/sri/foo.txt');
249             $file->move_to('/yada.txt');
250             say $file->slurp;
251              
252             =head1 DESCRIPTION
253              
254             L is a file content interface.
255              
256             =head1 ATTRIBUTES
257              
258             =head2 cleanup
259              
260             my $bool = $file->cleanup;
261             $file = $file->cleanup($bool);
262              
263             Delete L automatically once the file is not used anymore.
264              
265             =head2 handle
266              
267             my $handle = $file->handle;
268             $file = $file->handle(IO::File->new);
269              
270             Filehandle, created on demand.
271              
272             =head2 path
273              
274             my $path = $file->path;
275             $file = $file->path('/home/sri/foo.txt');
276              
277             File path used to create L, can also be automatically generated if
278             necessary.
279              
280             =head1 METHODS
281              
282             =head2 add_chunk
283              
284             $file = $file->add_chunk('foo bar baz');
285              
286             Add chunk of data, if there is range from range position to start writing.
287              
288             =head2 contains
289              
290             my $position = $file->contains('bar');
291              
292             Check if asset contains a specific string, if there is range from range position to start checking.
293              
294             =head2 get_chunk
295              
296             my $bytes = $file->get_chunk($offset);
297             my $bytes = $file->get_chunk($offset, $max);
298              
299             Get chunk of data starting from a specific position, defaults to a maximum
300             chunk size of C<131072> bytes (128KB).
301              
302             =head2 is_file
303              
304             my $true = $file->is_file;
305              
306             True.
307              
308             =head2 move_to
309              
310             $file = $file->move_to('/home/sri/bar.txt');
311              
312             Move asset data into a specific file and disable L.
313              
314             =head2 mtime
315              
316             my $mtime = $file->mtime;
317              
318             Modification time of asset.
319              
320             =head2 size
321              
322             my $size = $file->size;
323              
324             Size of asset data in bytes.
325              
326             =head2 md5sum
327              
328             my $md5 = $file->md5sum;
329              
330             return the md5 digest in hexadecimal form file;
331              
332             =head2 sha1sum
333              
334             my $sha1 = $file->sha1sum;
335              
336             return the sha1 digest in hexadecimal form file;
337              
338             =head2 slurp
339              
340             my $bytes = $file->slurp;
341              
342             Read all asset data at once.
343              
344             =head1 SEE ALSO
345              
346             L
347              
348             =cut