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   456 use namespace::autoclean;
  4         4  
  4         26  
4              
5 4     4   640 use Class::Null;
  4         236  
  4         85  
6 4     4   16 use English qw( -no_match_vars );
  4         5  
  4         30  
7 4     4   1306 use File::Copy;
  4         7  
  4         200  
8 4     4   16 use File::DataClass::Constants qw( EXCEPTION_CLASS FALSE NUL TRUE );
  4         7  
  4         184  
9 4         206 use File::DataClass::Functions qw( is_stale merge_file_data
10 4     4   16 merge_for_update throw );
  4         69  
11 4     4   18 use File::DataClass::Types qw( Bool HashRef Object Str );
  4         12  
  4         23  
12 4     4   2601 use Scalar::Util qw( blessed );
  4         5  
  4         156  
13 4     4   14 use Try::Tiny;
  4         4  
  4         179  
14 4         31 use Unexpected::Functions qw( RecordAlreadyExists PathNotFound
15 4     4   15 NothingUpdated Unspecified );
  4         5  
16 4     4   1335 use Moo;
  4         7  
  4         21  
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   69 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   35 has 'write_options' => is => 'ro', isa => HashRef, builder => sub { {} };
34              
35 17     17   16063 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 31 my ($self, $path, $result, $updating, $cond) = @_;
65              
66 18         71 my $rsrc_name = $result->result_source->name;
67              
68 18         44 $self->validate_params( $path, $rsrc_name ); my $updated;
  18         22  
69              
70 18         59 my $data = ($self->read_file( $path, TRUE ))[ 0 ];
71              
72             try {
73 18     18   442 my $filter = sub { $_get_src_attributes->( $cond, $_[ 0 ] ) };
  17         48  
74 18   100     315 my $id = $result->id; $data->{ $rsrc_name } //= {};
  18         120  
75              
76 18 100 66     67 not $updating and exists $data->{ $rsrc_name }->{ $id }
77             and throw RecordAlreadyExists, [ $path, $id ], level => 2;
78              
79             $updated = merge_for_update
80 17         72 ( \$data->{ $rsrc_name }->{ $id }, $result, $filter );
81             }
82 18     1   122 catch { $self->$_lock_reset( $path ); throw $_ };
  1         629  
  1         21  
83              
84 17 100       200 if ($updated) { $self->write_file( $path, $data, not $updating ) }
  15         81  
85 2         6 else { $self->$_lock_reset( $path ) }
86              
87 17 100       260 return $updated ? $result : FALSE;
88             }
89              
90             sub delete {
91 6     6 1 495 my ($self, $path, $result) = @_;
92              
93 6         22 my $rsrc_name = $result->result_source->name;
94              
95 6         14 $self->validate_params( $path, $rsrc_name );
96              
97 6         17 my $data = ($self->read_file( $path, TRUE ))[ 0 ]; my $id = $result->id;
  6         92  
98              
99 6 50 66     50 if (exists $data->{ $rsrc_name } and exists $data->{ $rsrc_name }->{ $id }) {
100 5         14 delete $data->{ $rsrc_name }->{ $id };
101 5 50       7 scalar keys %{ $data->{ $rsrc_name } } or delete $data->{ $rsrc_name };
  5         21  
102 5         17 $self->write_file( $path, $data );
103 5         67 return TRUE;
104             }
105              
106 1         3 $self->$_lock_reset( $path );
107 1         29 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         44  
  16         45  
  16         211  
112             }
113              
114             sub dump {
115 8     8 1 128 my ($self, $path, $data) = @_;
116              
117             return $self->txn_do( $path, sub {
118 8     8   54 $self->$_lock_set( $path ); $self->write_file( $path, $data, TRUE ) } );
  8         16  
  8         167  
119             }
120              
121             sub insert {
122 6     6 1 627 my ($self, $path, $result) = @_;
123              
124 6     4   41 return $self->create_or_update( $path, $result, FALSE, sub { TRUE } );
  4         14  
125             }
126              
127             sub load {
128 6 50   6 1 12 my ($self, @paths) = @_; $paths[ 0 ] or return {};
  6         23  
129              
130 6 100       61 scalar @paths == 1 and return ($self->read_file( $paths[ 0 ], FALSE ))[ 0 ];
131              
132 1         15 my ($loaded, $meta, $newest) = $self->_cache->get_by_paths( \@paths );
133 1         6 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         1 for my $path (@paths) {
140 2         5 my ($red, $path_mtime) = $self->read_file( $path, FALSE );
141              
142 2 100       4 $path_mtime > $newest and $newest = $path_mtime;
143 2         7 merge_file_data $loaded, $red;
144             }
145              
146 1         13 $self->_cache->set_by_paths( \@paths, $loaded, $self->meta_pack( $newest ) );
147 1         23 return $loaded;
148             }
149              
150             sub meta_pack { # Modified in a subclass
151 34     34 1 74 my ($self, $mtime) = @_; return { mtime => $mtime };
  34         231  
152             }
153              
154             sub meta_unpack { # Modified in a subclass
155 83 100   83 1 95 my ($self, $attr) = @_; return $attr ? $attr->{mtime} : undef;
  83         177  
156             }
157              
158             sub read_file {
159 82     82 1 91 my ($self, $path, $for_update) = @_;
160              
161 82         148 $self->$_lock_set( $path ); my ($data, $path_mtime);
  82         1518  
162              
163             try {
164 82 100   82   2266 my $stat = $path->stat; defined $stat and $path_mtime = $stat->{mtime};
  82         327  
165              
166 82         71 my $meta; ($data, $meta) = $self->_cache->get( $path );
  82         1272  
167              
168 82         582 my $cache_mtime = $self->meta_unpack( $meta );
169              
170 82 100       247 if (is_stale $data, $cache_mtime, $path_mtime) {
171 36 50 66     98 if ($for_update and not $path->exists) {
172 0         0 $data = {}; # uncoverable statement
173             }
174             else {
175 36         281 $data = $self->read_from_file( $path->lock ); $path->close;
  33         81  
176 33         98 $meta = $self->meta_pack( $path_mtime );
177 33         576 $self->_cache->set( $path, $data, $meta );
178 33         881 $self->_log->debug( "Read file ${path}" );
179             }
180             }
181 46         821 else { $self->_log->debug( "Read cache ${path}" ) }
182             }
183 82     3   552 catch { $self->$_lock_reset( $path ); throw $_ };
  3         14693  
  3         69  
184              
185 79 100       3138 $for_update or $self->$_lock_reset( $path );
186              
187 79         1177 return ($data, $path_mtime);
188             }
189              
190             sub read_from_file {
191 1     1 1 47 throw 'Method [_1] not overridden in subclass [_2]',
192             [ 'read_from_file', blessed $_[ 0 ] ];
193             }
194              
195             sub select {
196 43     43 1 3898 my ($self, $path, $rsrc_name) = @_;
197              
198 43         110 $self->validate_params( $path, $rsrc_name );
199              
200 43         112 my $data = ($self->read_file( $path, FALSE ))[ 0 ];
201              
202 43 100       405 return exists $data->{ $rsrc_name } ? $data->{ $rsrc_name } : {};
203             }
204              
205             sub txn_do {
206 57     57 1 4821 my ($self, $path, $code_ref) = @_;
207              
208 57         78 my $wantarray = wantarray; $self->validate_params( $path, TRUE );
  57         145  
209              
210 57         150 my $key = "txn:${path}"; $self->$_lock_set( $key ); my $res;
  57         1126  
  57         61  
211              
212             try {
213 57 100   57   1652 if ($wantarray) { $res = [ $code_ref->() ] }
  1         3  
214 56         126 else { $res = $code_ref->() }
215             }
216 57     5   366 catch { $self->$_lock_reset( $key ); throw $_, { level => 4 } };
  5         3325  
  5         21  
217              
218 52         1017 $self->$_lock_reset( $key );
219              
220 52 100       312 return $wantarray ? @{ $res } : $res;
  1         5  
221             }
222              
223             sub update {
224 11     11 1 1077 my ($self, $path, $result, $updating, $cond) = @_;
225              
226 11   50 15   48 $updating //= TRUE; $cond //= sub { TRUE };
  11   50     58  
  15         43  
227              
228 11 100       32 my $updated = $self->create_or_update( $path, $result, $updating, $cond )
229             or throw NothingUpdated, level => 2;
230              
231 10         49 return $updated;
232             }
233              
234             sub validate_params {
235 125     125 1 127 my ($self, $path, $rsrc_name) = @_;
236              
237 125 50       358 $path or throw Unspecified, [ 'path name' ], level => 2;
238 125 50       882 blessed $path or throw 'Path [_1] is not blessed', [ $path ], level => 2;
239 125 50       207 $rsrc_name or throw 'Path [_1] result source not specified', [ $path ],
240             level => 2;
241              
242 125         112 return;
243             }
244              
245             sub write_file {
246 28     28 1 57 my ($self, $path, $data, $create) = @_; my $exists = $path->exists;
  28         84  
247              
248             try {
249 28 50 66 28   824 $create or $exists or throw PathNotFound, [ $path ];
250 28 100       192 $exists or $path->perms( $self->_perms );
251 28 50       181 $self->atomic_write and $path->atomic;
252              
253 28 100 100     180 if ($exists and $self->backup and not $path->empty) {
      66        
254 3 50       9 copy( "${path}", $path.$self->backup )
255             or throw 'Backup copy failed: [_1]', [ $OS_ERROR ];
256             }
257              
258 28         719 try { $data = $self->write_to_file( $path->lock, $data ); $path->close }
  27         74  
259 28         742 catch { $path->delete; throw $_ };
  1         12  
  1         4  
260              
261 27         709 $self->_cache->remove( $path );
262 27         595 $self->_log->debug( "Write file ${path}" )
263             }
264 28     1   1099 catch { $self->$_lock_reset( $path ); throw $_ };
  1         8789  
  1         27  
265              
266 27         1007 $self->$_lock_reset( $path );
267 27         520 return $data;
268             }
269              
270             sub write_to_file {
271 1     1 1 1426 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   1258 throw 'Class [_1] should never call _read_file', [ blessed $_[ 0 ] ];
278             }
279              
280             sub _write_file {
281 1     1   1267 throw 'Class [_1] should never call _write_file', [ blessed $_[ 0 ] ];
282             }
283              
284             1;
285              
286             __END__