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   475 use namespace::autoclean;
  4         4  
  4         25  
4              
5 4     4   599 use Class::Null;
  4         234  
  4         80  
6 4     4   15 use English qw( -no_match_vars );
  4         4  
  4         29  
7 4     4   1487 use File::Copy;
  4         5  
  4         215  
8 4     4   19 use File::DataClass::Constants qw( EXCEPTION_CLASS FALSE NUL TRUE );
  4         4  
  4         180  
9 4         199 use File::DataClass::Functions qw( is_stale merge_file_data
10 4     4   14 merge_for_update throw );
  4         5  
11 4     4   14 use File::DataClass::Types qw( Bool HashRef Object Str );
  4         6  
  4         25  
12 4     4   2547 use Scalar::Util qw( blessed );
  4         5  
  4         162  
13 4     4   15 use Try::Tiny;
  4         5  
  4         164  
14 4         39 use Unexpected::Functions qw( RecordAlreadyExists PathNotFound
15 4     4   14 NothingUpdated Unspecified );
  4         4  
16 4     4   1349 use Moo;
  4         6  
  4         19  
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   82 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   41 has 'write_options' => is => 'ro', isa => HashRef, builder => sub { {} };
34              
35 17     17   16657 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 27 my ($self, $path, $result, $updating, $cond) = @_;
65              
66 18         70 my $rsrc_name = $result->result_source->name;
67              
68 18         49 $self->validate_params( $path, $rsrc_name ); my $updated;
  18         17  
69              
70 18         57 my $data = ($self->read_file( $path, TRUE ))[ 0 ];
71              
72             try {
73 18     18   443 my $filter = sub { $_get_src_attributes->( $cond, $_[ 0 ] ) };
  17         49  
74 18   100     291 my $id = $result->id; $data->{ $rsrc_name } //= {};
  18         1503  
75              
76 18 100 66     69 not $updating and exists $data->{ $rsrc_name }->{ $id }
77             and throw RecordAlreadyExists, [ $path, $id ], level => 2;
78              
79             $updated = merge_for_update
80 17         74 ( \$data->{ $rsrc_name }->{ $id }, $result, $filter );
81             }
82 18     1   133 catch { $self->$_lock_reset( $path ); throw $_ };
  1         778  
  1         25  
83              
84 17 100       211 if ($updated) { $self->write_file( $path, $data, not $updating ) }
  15         60  
85 2         7 else { $self->$_lock_reset( $path ) }
86              
87 17 100       289 return $updated ? $result : FALSE;
88             }
89              
90             sub delete {
91 6     6 1 627 my ($self, $path, $result) = @_;
92              
93 6         27 my $rsrc_name = $result->result_source->name;
94              
95 6         19 $self->validate_params( $path, $rsrc_name );
96              
97 6         18 my $data = ($self->read_file( $path, TRUE ))[ 0 ]; my $id = $result->id;
  6         104  
98              
99 6 50 66     61 if (exists $data->{ $rsrc_name } and exists $data->{ $rsrc_name }->{ $id }) {
100 5         10 delete $data->{ $rsrc_name }->{ $id };
101 5 50       7 scalar keys %{ $data->{ $rsrc_name } } or delete $data->{ $rsrc_name };
  5         26  
102 5         17 $self->write_file( $path, $data );
103 5         121 return TRUE;
104             }
105              
106 1         3 $self->$_lock_reset( $path );
107 1         30 return FALSE;
108             }
109              
110             sub DEMOLISH {
111 16 50   16 1 4373 my ($self, $gd) = @_; $gd and return; $self->$_lock_reset_all(); return;
  16         48  
  16         47  
  16         210  
112             }
113              
114             sub dump {
115 8     8 1 151 my ($self, $path, $data) = @_;
116              
117             return $self->txn_do( $path, sub {
118 8     8   48 $self->$_lock_set( $path ); $self->write_file( $path, $data, TRUE ) } );
  8         19  
  8         182  
119             }
120              
121             sub insert {
122 6     6 1 2409 my ($self, $path, $result) = @_;
123              
124 6     4   45 return $self->create_or_update( $path, $result, FALSE, sub { TRUE } );
  4         15  
125             }
126              
127             sub load {
128 6 50   6 1 65 my ($self, @paths) = @_; $paths[ 0 ] or return {};
  6         25  
129              
130 6 100       67 scalar @paths == 1 and return ($self->read_file( $paths[ 0 ], FALSE ))[ 0 ];
131              
132 1         25 my ($loaded, $meta, $newest) = $self->_cache->get_by_paths( \@paths );
133 1         5 my $cache_mtime = $self->meta_unpack( $meta );
134              
135 1 50       4 not is_stale $loaded, $cache_mtime, $newest and return $loaded;
136              
137 1         2 $loaded = {}; $newest = 0;
  1         1  
138              
139 1         3 for my $path (@paths) {
140 2         10 my ($red, $path_mtime) = $self->read_file( $path, FALSE );
141              
142 2 100       6 $path_mtime > $newest and $newest = $path_mtime;
143 2         7 merge_file_data $loaded, $red;
144             }
145              
146 1         15 $self->_cache->set_by_paths( \@paths, $loaded, $self->meta_pack( $newest ) );
147 1         6 return $loaded;
148             }
149              
150             sub meta_pack { # Modified in a subclass
151 34     34 1 87 my ($self, $mtime) = @_; return { mtime => $mtime };
  34         88  
152             }
153              
154             sub meta_unpack { # Modified in a subclass
155 83 100   83 1 134 my ($self, $attr) = @_; return $attr ? $attr->{mtime} : undef;
  83         183  
156             }
157              
158             sub read_file {
159 82     82 1 98 my ($self, $path, $for_update) = @_;
160              
161 82         149 $self->$_lock_set( $path ); my ($data, $path_mtime);
  82         1695  
162              
163             try {
164 82 100   82   2332 my $stat = $path->stat; defined $stat and $path_mtime = $stat->{mtime};
  82         361  
165              
166 82         71 my $meta; ($data, $meta) = $self->_cache->get( $path );
  82         1259  
167              
168 82         628 my $cache_mtime = $self->meta_unpack( $meta );
169              
170 82 100       252 if (is_stale $data, $cache_mtime, $path_mtime) {
171 36 50 66     107 if ($for_update and not $path->exists) {
172 0         0 $data = {}; # uncoverable statement
173             }
174             else {
175 36         333 $data = $self->read_from_file( $path->lock ); $path->close;
  33         85  
176 33         110 $meta = $self->meta_pack( $path_mtime );
177 33         596 $self->_cache->set( $path, $data, $meta );
178 33         945 $self->_log->debug( "Read file ${path}" );
179             }
180             }
181 46         867 else { $self->_log->debug( "Read cache ${path}" ) }
182             }
183 82     3   584 catch { $self->$_lock_reset( $path ); throw $_ };
  3         15912  
  3         75  
184              
185 79 100       3398 $for_update or $self->$_lock_reset( $path );
186              
187 79         1259 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 4506 my ($self, $path, $rsrc_name) = @_;
197              
198 43         87 $self->validate_params( $path, $rsrc_name );
199              
200 43         115 my $data = ($self->read_file( $path, FALSE ))[ 0 ];
201              
202 43 100       434 return exists $data->{ $rsrc_name } ? $data->{ $rsrc_name } : {};
203             }
204              
205             sub txn_do {
206 57     57 1 7714 my ($self, $path, $code_ref) = @_;
207              
208 57         83 my $wantarray = wantarray; $self->validate_params( $path, TRUE );
  57         159  
209              
210 57         153 my $key = "txn:${path}"; $self->$_lock_set( $key ); my $res;
  57         1231  
  57         65  
211              
212             try {
213 57 100   57   1712 if ($wantarray) { $res = [ $code_ref->() ] }
  1         3  
214 56         120 else { $res = $code_ref->() }
215             }
216 57     5   359 catch { $self->$_lock_reset( $key ); throw $_, { level => 4 } };
  5         3369  
  5         20  
217              
218 52         1261 $self->$_lock_reset( $key );
219              
220 52 100       286 return $wantarray ? @{ $res } : $res;
  1         5  
221             }
222              
223             sub update {
224 11     11 1 1248 my ($self, $path, $result, $updating, $cond) = @_;
225              
226 11   50 15   49 $updating //= TRUE; $cond //= sub { TRUE };
  11   50     61  
  15         42  
227              
228 11 100       39 my $updated = $self->create_or_update( $path, $result, $updating, $cond )
229             or throw NothingUpdated, level => 2;
230              
231 10         63 return $updated;
232             }
233              
234             sub validate_params {
235 125     125 1 137 my ($self, $path, $rsrc_name) = @_;
236              
237 125 50       410 $path or throw Unspecified, [ 'path name' ], level => 2;
238 125 50       982 blessed $path or throw 'Path [_1] is not blessed', [ $path ], level => 2;
239 125 50       235 $rsrc_name or throw 'Path [_1] result source not specified', [ $path ],
240             level => 2;
241              
242 125         126 return;
243             }
244              
245             sub write_file {
246 28     28 1 43 my ($self, $path, $data, $create) = @_; my $exists = $path->exists;
  28         75  
247              
248             try {
249 28 50 66 28   723 $create or $exists or throw PathNotFound, [ $path ];
250 28 100       177 $exists or $path->perms( $self->_perms );
251 28 50       208 $self->atomic_write and $path->atomic;
252              
253 28 100 100     168 if ($exists and $self->backup and not $path->empty) {
      66        
254 3 50       11 copy( "${path}", $path.$self->backup )
255             or throw 'Backup copy failed: [_1]', [ $OS_ERROR ];
256             }
257              
258 28         715 try { $data = $self->write_to_file( $path->lock, $data ); $path->close }
  27         87  
259 28         967 catch { $path->delete; throw $_ };
  1         12  
  1         5  
260              
261 27         795 $self->_cache->remove( $path );
262 27         667 $self->_log->debug( "Write file ${path}" )
263             }
264 28     1   1148 catch { $self->$_lock_reset( $path ); throw $_ };
  1         9593  
  1         25  
265              
266 27         1088 $self->$_lock_reset( $path );
267 27         577 return $data;
268             }
269              
270             sub write_to_file {
271 1     1 1 1810 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   1459 throw 'Class [_1] should never call _read_file', [ blessed $_[ 0 ] ];
278             }
279              
280             sub _write_file {
281 1     1   1292 throw 'Class [_1] should never call _write_file', [ blessed $_[ 0 ] ];
282             }
283              
284             1;
285              
286             __END__