File Coverage

blib/lib/Asset/File.pm
Criterion Covered Total %
statement 103 108 95.3
branch 24 30 80.0
condition 13 16 81.2
subroutine 22 23 95.6
pod 8 12 66.6
total 170 189 89.9


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