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 10 12 83.3
total 172 189 91.0


line stmt bran cond sub pod time code
1             package Asset::File;
2 1     1   16652 use Moo;
  1         11350  
  1         5  
3 1     1   1196 use Carp 'croak';
  1         2  
  1         81  
4 1     1   449 use Errno 'EEXIST';
  1         1035  
  1         96  
5 1     1   4 use Fcntl qw(O_APPEND O_CREAT O_EXCL O_RDONLY O_RDWR);
  1         2  
  1         53  
6 1     1   508 use File::Copy 'move';
  1         3888  
  1         60  
7 1     1   6 use File::Spec::Functions 'catfile';
  1         2  
  1         59  
8 1     1   757 use File::Temp;
  1         15058  
  1         76  
9 1     1   7 use Digest::MD5 'md5_hex';
  1         1  
  1         44  
10 1     1   547 use Digest::SHA1;
  1         625  
  1         44  
11 1     1   505 use IO::File;
  1         782  
  1         1315  
12             our $VERSION = '1.02';
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   1097 my $self = shift;
59 15 100 66     207 return unless $self->cleanup && defined(my $path = $self->path);
60 9         165 close $self->handle;
61 9 50       991 unlink $path if -w $path;
62             }
63              
64 3   100 3 0 27 sub is_range { !!($_[0]->end_range || $_[0]->start_range) };
65              
66             sub add_chunk {
67 19     19 1 1034 my ($self, $chunk) = @_;
68 19   50     41 $chunk //= '';
69 19         400 my $handle = $self->handle;
70 19 100       388 if ($self->start_range) {
71 1         5 $handle->sysseek($self->start_range, SEEK_SET);
72             }
73             else {
74 18         51 $handle->sysseek(0, SEEK_END);
75             }
76 19 50       117 croak "Can't write to asset: $!"
77             unless defined $handle->syswrite($chunk, length $chunk);
78 19         1269 return $self;
79             }
80              
81             sub contains {
82 37     37 1 347 my ($self, $str) = @_;
83              
84 37         812 my $handle = $self->handle;
85 37         296 $handle->sysseek($self->start_range, SEEK_SET);
86              
87             # Calculate window size
88 37   100     252 my $end = $self->end_range // $self->size;
89 37         342 my $len = length $str;
90 37 100       58 my $size = $len > 131072 ? $len : 131072;
91 37 100       93 $size = $end - $self->start_range if $size > $end - $self->start_range;
92              
93             # Sliding window search
94 37         33 my $offset = 0;
95 37         84 my $start = $handle->sysread(my $window, $len);
96 37         395 while ($offset < $end) {
97              
98             # Read as much as possible
99 55         60 my $diff = $end - ($start + $offset);
100 55 100       138 my $read = $handle->sysread(my $buffer, $diff < $size ? $diff : $size);
101 55         1724 $window .= $buffer;
102              
103             # Search window
104 55         3552 my $pos = index $window, $str;
105 55 100       249 return $offset + $pos if $pos >= 0;
106 26 100 66     130 return -1 if $read == 0 || ($offset += $read) == $end;
107              
108             # Resize window
109 19         47 substr $window, 0, $read, '';
110             }
111              
112 1         4 return -1;
113             }
114              
115             sub get_chunk {
116 8     8 1 11 my ($self, $offset, $max) = @_;
117 8   100     24 $max //= 131072;
118              
119 8         14 $offset += $self->start_range;
120 8         179 my $handle = $self->handle;
121 8         52 $handle->sysseek($offset, SEEK_SET);
122              
123 8         37 my $buffer;
124 8 100       24 if (defined(my $end = $self->end_range)) {
125 7 50       19 return '' if (my $chunk = $end + 1 - $offset) <= 0;
126 7 100       24 $handle->sysread($buffer, $chunk > $max ? $max : $chunk);
127             }
128 1         5 else { $handle->sysread($buffer, $max) }
129              
130 8         81 return $buffer;
131             }
132              
133             sub first_line_of {
134 0     0 0 0 my $fh = shift->handle;
135 0         0 my $line = <$fh>;
136 0         0 chomp $line;
137 0         0 $line =~ s/^\s+|\s+$//g;
138              
139 0         0 return $line;
140             }
141              
142             sub md5sum {
143 1     1 1 6 my $self = shift;
144 1         9 my $md5 = Digest::MD5->new;
145 1         24 my $handle = $self->handle;
146 1         8 $handle->sysseek(0, SEEK_SET);
147 1         6 while ($handle->sysread(my $buffer, 131072, 0)) {
148 1         17 $md5->add($buffer);
149             }
150 1         14 return $md5->hexdigest,
151             }
152              
153             sub sha1sum {
154 1     1 1 2 my $self = shift;
155 1         8 my $sha1 = Digest::SHA1->new;
156 1         24 my $handle = $self->handle;
157 1         8 $handle->sysseek(0, SEEK_SET);
158 1         7 while ($handle->sysread(my $buffer, 131072, 0)) {
159 1         33 $sha1->add($buffer);
160             }
161 1         17 return $sha1->hexdigest,
162             }
163              
164 1     1 1 8 sub is_file {1}
165              
166             sub move_to {
167 2     2 1 6 my ($self, $to) = @_;
168              
169             # Windows requires that the handle is closed
170 2         40 close $self->handle;
171 2         94 delete $self->{handle};
172              
173             # Move file and prevent clean up
174 2         7 my $from = $self->path;
175 2 50       7 move($from, $to) or croak qq{Can't move file "$from" to "$to": $!};
176 2         158 $self->cleanup(0);
177 2         4 $self->path($to);
178 2         8 return $self;
179             }
180              
181 3     3 1 1125 sub mtime { (stat shift->handle)[9] }
182              
183 27     27 1 433 sub size { -s shift->handle }
184              
185             sub slurp {
186 5 50   5 1 534 return '' unless defined (my $path = shift->path);
187 5 50       134 croak qq{Can't open file "$path": $!} unless open my $file, '<', $path;
188 5         7 my $content = '';
189 5         23 while ($file->sysread(my $buffer, 131072, 0)) { $content .= $buffer }
  3         32  
190 5         110 return $content;
191             }
192              
193             1;
194              
195             =encoding utf8
196              
197             =head1 NAME
198              
199             Asset::File - File Operation interface
200              
201             =head1 SYNOPSIS
202              
203             use Asset::File;
204            
205             # Temporary file
206             my $file = Asset::File->new;
207             $file->add_chunk('foo bar baz');
208             say 'File contains "bar"' if $file->contains('bar') >= 0;
209             say $file->slurp;
210            
211             # Existing file
212             my $file = Asset::File->new(path => '/home/sri/foo.txt');
213             $file->move_to('/yada.txt');
214             say $file->slurp;
215              
216             =head1 DESCRIPTION
217              
218             L is a file content interface.
219              
220             =head1 ATTRIBUTES
221              
222             =head2 cleanup
223              
224             my $bool = $file->cleanup;
225             $file = $file->cleanup($bool);
226              
227             Delete L automatically once the file is not used anymore.
228              
229             =head2 handle
230              
231             my $handle = $file->handle;
232             $file = $file->handle(IO::File->new);
233              
234             Filehandle, created on demand.
235              
236             =head2 path
237              
238             my $path = $file->path;
239             $file = $file->path('/home/sri/foo.txt');
240              
241             File path used to create L, can also be automatically generated if
242             necessary.
243              
244             =head1 METHODS
245              
246             =head2 add_chunk
247              
248             $file = $file->add_chunk('foo bar baz');
249              
250             Add chunk of data, if there is range from range position to start writing.
251              
252             =head2 contains
253              
254             my $position = $file->contains('bar');
255              
256             Check if asset contains a specific string, if there is range from range position to start checking.
257              
258             =head2 get_chunk
259              
260             my $bytes = $file->get_chunk($offset);
261             my $bytes = $file->get_chunk($offset, $max);
262              
263             Get chunk of data starting from a specific position, defaults to a maximum
264             chunk size of C<131072> bytes (128KB).
265              
266             =head2 is_file
267              
268             my $true = $file->is_file;
269              
270             True.
271              
272             =head2 move_to
273              
274             $file = $file->move_to('/home/sri/bar.txt');
275              
276             Move asset data into a specific file and disable L.
277              
278             =head2 mtime
279              
280             my $mtime = $file->mtime;
281              
282             Modification time of asset.
283              
284             =head2 size
285              
286             my $size = $file->size;
287              
288             Size of asset data in bytes.
289              
290             =head2 md5sum
291              
292             my $md5 = $file->md5sum;
293              
294             return the md5 digest in hexadecimal form file;
295              
296             =head2 sha1sum
297              
298             my $sha1 = $file->sha1sum;
299              
300             return the sha1 digest in hexadecimal form file;
301              
302             =head2 slurp
303              
304             my $bytes = $file->slurp;
305              
306             Read all asset data at once.
307              
308             =head1 SEE ALSO
309              
310             L
311              
312             =cut