File Coverage

lib/File/DataClass/Storage.pm
Criterion Covered Total %
statement 170 171 100.0
branch 42 54 77.7
condition 17 24 70.8
subroutine 43 43 100.0
pod 16 16 100.0
total 288 308 93.8


line stmt bran cond sub pod time code
1             package File::DataClass::Storage;
2              
3 4     4   521 use namespace::autoclean;
  4         9  
  4         29  
4              
5 4     4   667 use Class::Null;
  4         293  
  4         104  
6 4     4   21 use English qw( -no_match_vars );
  4         10  
  4         32  
7 4     4   1319 use File::Copy;
  4         8  
  4         212  
8 4     4   23 use File::DataClass::Constants qw( EXCEPTION_CLASS FALSE NUL TRUE );
  4         51  
  4         191  
9 4         196 use File::DataClass::Functions qw( is_stale merge_file_data
10 4     4   18 merge_for_update throw );
  4         40  
11 4     4   24 use File::DataClass::Types qw( Bool HashRef Object Str );
  4         7  
  4         29  
12 4     4   3925 use Scalar::Util qw( blessed );
  4         8  
  4         185  
13 4     4   27 use Try::Tiny;
  4         9  
  4         191  
14 4         38 use Unexpected::Functions qw( RecordAlreadyExists PathNotFound
15 4     4   21 NothingUpdated Unspecified );
  4         9  
16 4     4   1738 use Moo;
  4         52  
  4         24  
17              
18             has 'atomic_write' => is => 'ro', isa => Bool, default => TRUE;
19              
20             has 'backup' => is => 'ro', isa => Str, default => NUL;
21              
22             has 'encoding' => is => 'ro', isa => Str, default => NUL;
23              
24             has 'extn' => is => 'ro', isa => Str, default => NUL;
25              
26 1     1   110 has 'read_options' => is => 'ro', isa => HashRef, builder => sub { {} };
27              
28             has 'schema' => is => 'ro', isa => Object,
29             handles => { _cache => 'cache', _lock => 'lock',
30             _log => 'log', _perms => 'perms', },
31             required => TRUE, weak_ref => TRUE;
32              
33 1     1   55 has 'write_options' => is => 'ro', isa => HashRef, builder => sub { {} };
34              
35 17     17   19795 has '_locks' => is => 'ro', isa => HashRef, builder => sub { {} };
36              
37             # Private functions
38             my $_get_src_attributes = sub {
39             my ($cond, $src) = @_;
40              
41             return grep { not m{ \A _ }mx
42             and $_ ne 'id' and $_ ne 'name'
43             and $cond->( $_ ) } keys %{ $src };
44             };
45              
46             my $_lock_set = sub {
47             $_[ 0 ]->_lock->set( k => $_[ 1 ] ); $_[ 0 ]->_locks->{ $_[ 1 ] } = TRUE;
48             };
49              
50             my $_lock_reset = sub {
51             $_[ 0 ]->_lock->reset( k => $_[ 1 ] ); delete $_[ 0 ]->_locks->{ $_[ 1 ] };
52             };
53              
54             my $_lock_reset_all = sub {
55             my $self = shift;
56              
57             eval { $self->$_lock_reset( $_ ) } for (keys %{ $self->_locks });
58              
59             return;
60             };
61              
62             # Public methods
63             sub create_or_update {
64 18     18 1 44 my ($self, $path, $result, $updating, $cond) = @_;
65              
66 18         77 my $rsrc_name = $result->result_source->name;
67              
68 18         59 $self->validate_params( $path, $rsrc_name ); my $updated;
  18         30  
69              
70 18         66 my $data = ($self->read_file( $path, TRUE ))[ 0 ];
71              
72             try {
73 18     18   707 my $filter = sub { $_get_src_attributes->( $cond, $_[ 0 ] ) };
  17         54  
74 18   100     334 my $id = $result->id; $data->{ $rsrc_name } //= {};
  18         160  
75              
76 18 100 66     65 not $updating and exists $data->{ $rsrc_name }->{ $id }
77             and throw RecordAlreadyExists, [ $path, $id ], level => 2;
78              
79             $updated = merge_for_update
80 17         70 ( \$data->{ $rsrc_name }->{ $id }, $result, $filter );
81             }
82 18     1   139 catch { $self->$_lock_reset( $path ); throw $_ };
  1         1013  
  1         27  
83              
84 17 100       264 if ($updated) { $self->write_file( $path, $data, not $updating ) }
  15         61  
85 2         10 else { $self->$_lock_reset( $path ) }
86              
87 17 100       281 return $updated ? $result : FALSE;
88             }
89              
90             sub delete {
91 6     6 1 573 my ($self, $path, $result) = @_;
92              
93 6         26 my $rsrc_name = $result->result_source->name;
94              
95 6         23 $self->validate_params( $path, $rsrc_name );
96              
97 6         22 my $data = ($self->read_file( $path, TRUE ))[ 0 ]; my $id = $result->id;
  6         106  
98              
99 6 50 66     63 if (exists $data->{ $rsrc_name } and exists $data->{ $rsrc_name }->{ $id }) {
100 5         15 delete $data->{ $rsrc_name }->{ $id };
101 5 50       10 scalar keys %{ $data->{ $rsrc_name } } or delete $data->{ $rsrc_name };
  5         23  
102 5         24 $self->write_file( $path, $data );
103 5         69 return TRUE;
104             }
105              
106 1         4 $self->$_lock_reset( $path );
107 1         35 return FALSE;
108             }
109              
110             sub DEMOLISH {
111 16 50   16 1 5294 my ($self, $gd) = @_; $gd and return; $self->$_lock_reset_all(); return;
  16         54  
  16         64  
  16         221  
112             }
113              
114             sub dump {
115 8     8 1 189 my ($self, $path, $data) = @_;
116              
117             return $self->txn_do( $path, sub {
118 8     8   67 $self->$_lock_set( $path ); $self->write_file( $path, $data, TRUE ) } );
  8         22  
  8         238  
119             }
120              
121             sub insert {
122 6     6 1 741 my ($self, $path, $result) = @_;
123              
124 6     4   41 return $self->create_or_update( $path, $result, FALSE, sub { TRUE } );
  4         16  
125             }
126              
127             sub load {
128 6 50   6 1 22 my ($self, @paths) = @_; $paths[ 0 ] or return {};
  6         25  
129              
130 6 100       78 scalar @paths == 1 and return ($self->read_file( $paths[ 0 ], FALSE ))[ 0 ];
131              
132 1         18 my ($loaded, $meta, $newest) = $self->_cache->get_by_paths( \@paths );
133 1         4 my $cache_mtime = $self->meta_unpack( $meta );
134              
135 1 50       5 not is_stale $loaded, $cache_mtime, $newest and return $loaded;
136              
137 1         2 $loaded = {}; $newest = 0;
  1         2  
138              
139 1         4 for my $path (@paths) {
140 2         7 my ($red, $path_mtime) = $self->read_file( $path, FALSE );
141              
142 2 100       7 $path_mtime > $newest and $newest = $path_mtime;
143 2         9 merge_file_data $loaded, $red;
144             }
145              
146 1         15 $self->_cache->set_by_paths( \@paths, $loaded, $self->meta_pack( $newest ) );
147 1         37 return $loaded;
148             }
149              
150             sub meta_pack { # Modified in a subclass
151 34     34 1 179 my ($self, $mtime) = @_; return { mtime => $mtime };
  34         111  
152             }
153              
154             sub meta_unpack { # Modified in a subclass
155 83 100   83 1 209 my ($self, $attr) = @_; return $attr ? $attr->{mtime} : undef;
  83         247  
156             }
157              
158             sub read_file {
159 82     82 1 202 my ($self, $path, $for_update) = @_;
160              
161 82         251 $self->$_lock_set( $path ); my ($data, $path_mtime);
  82         2159  
162              
163             try {
164 82 100   82   3657 my $stat = $path->stat; defined $stat and $path_mtime = $stat->{mtime};
  82         268  
165              
166 82         142 my $meta; ($data, $meta) = $self->_cache->get( $path );
  82         1333  
167              
168 82         717 my $cache_mtime = $self->meta_unpack( $meta );
169              
170 82 100       302 if (is_stale $data, $cache_mtime, $path_mtime) {
171 36 50 66     121 if ($for_update and not $path->exists) {
172 0         0 $data = {}; # uncoverable statement
173             }
174             else {
175 36         373 $data = $self->read_from_file( $path->lock ); $path->close;
  33         125  
176 33         112 $meta = $self->meta_pack( $path_mtime );
177 33         673 $self->_cache->set( $path, $data, $meta );
178 33         1073 $self->_log->debug( "Read file ${path}" );
179             }
180             }
181 46         908 else { $self->_log->debug( "Read cache ${path}" ) }
182             }
183 82     3   648 catch { $self->$_lock_reset( $path ); throw $_ };
  3         18246  
  3         95  
184              
185 79 100       4167 $for_update or $self->$_lock_reset( $path );
186              
187 79         1573 return ($data, $path_mtime);
188             }
189              
190             sub read_from_file {
191 1     1 1 66 throw 'Method [_1] not overridden in subclass [_2]',
192             [ 'read_from_file', blessed $_[ 0 ] ];
193             }
194              
195             sub select {
196 43     43 1 5500 my ($self, $path, $rsrc_name) = @_;
197              
198 43         130 $self->validate_params( $path, $rsrc_name );
199              
200 43         162 my $data = ($self->read_file( $path, FALSE ))[ 0 ];
201              
202 43 100       493 return exists $data->{ $rsrc_name } ? $data->{ $rsrc_name } : {};
203             }
204              
205             sub txn_do {
206 57     57 1 6426 my ($self, $path, $code_ref) = @_;
207              
208 57         112 my $wantarray = wantarray; $self->validate_params( $path, TRUE );
  57         201  
209              
210 57         190 my $key = "txn:${path}"; $self->$_lock_set( $key ); my $res;
  57         1507  
  57         156  
211              
212             try {
213 57 100   57   2577 if ($wantarray) { $res = [ $code_ref->() ] }
  1         4  
214 56         170 else { $res = $code_ref->() }
215             }
216 57     5   384 catch { $self->$_lock_reset( $key ); throw $_, { level => 4 } };
  5         4743  
  5         22  
217              
218 52         1400 $self->$_lock_reset( $key );
219              
220 52 100       335 return $wantarray ? @{ $res } : $res;
  1         7  
221             }
222              
223             sub update {
224 11     11 1 1424 my ($self, $path, $result, $updating, $cond) = @_;
225              
226 11   50 15   62 $updating //= TRUE; $cond //= sub { TRUE };
  11   50     116  
  15         48  
227              
228 11 100       41 my $updated = $self->create_or_update( $path, $result, $updating, $cond )
229             or throw NothingUpdated, level => 2;
230              
231 10         61 return $updated;
232             }
233              
234             sub validate_params {
235 125     125 1 250 my ($self, $path, $rsrc_name) = @_;
236              
237 125 50       476 $path or throw Unspecified, [ 'path name' ], level => 2;
238 125 50       1401 blessed $path or throw 'Path [_1] is not blessed', [ $path ], level => 2;
239 125 50       331 $rsrc_name or throw 'Path [_1] result source not specified', [ $path ],
240             level => 2;
241              
242 125         251 return;
243             }
244              
245             sub write_file {
246 28     28 1 77 my ($self, $path, $data, $create) = @_; my $exists = $path->exists;
  28         112  
247              
248             try {
249 28 50 66 28   1214 $create or $exists or throw PathNotFound, [ $path ];
250 28 100       203 $exists or $path->perms( $self->_perms );
251 28 50       186 $self->atomic_write and $path->atomic;
252              
253 28 100 100     179 if ($exists and $self->backup and not $path->empty) {
      66        
254 3 50       13 copy( "${path}", $path.$self->backup )
255             or throw 'Backup copy failed: [_1]', [ $OS_ERROR ];
256             }
257              
258 28         1134 try { $data = $self->write_to_file( $path->lock, $data ); $path->close }
  27         94  
259 28         877 catch { $path->delete; throw $_ };
  1         19  
  1         5  
260              
261 27         824 $self->_cache->remove( $path );
262 27         734 $self->_log->debug( "Write file ${path}" )
263             }
264 28     1   1365 catch { $self->$_lock_reset( $path ); throw $_ };
  1         10547  
  1         31  
265              
266 27         1309 $self->$_lock_reset( $path );
267 27         708 return $data;
268             }
269              
270             sub write_to_file {
271 1     1 1 1997 throw 'Method [_1] not overridden in subclass [_2]',
272             [ 'write_to_file', blessed $_[ 0 ] ];
273             }
274              
275             # Backcompat
276             sub _read_file {
277 1     1   1968 throw 'Class [_1] should never call _read_file', [ blessed $_[ 0 ] ];
278             }
279              
280             sub _write_file {
281 1     1   1840 throw 'Class [_1] should never call _write_file', [ blessed $_[ 0 ] ];
282             }
283              
284             1;
285              
286             __END__