File Coverage

blib/lib/Articulate/Storage/Local.pm
Criterion Covered Total %
statement 130 176 73.8
branch 35 76 46.0
condition 6 13 46.1
subroutine 27 32 84.3
pod 14 18 77.7
total 212 315 67.3


line stmt bran cond sub pod time code
1             package Articulate::Storage::Local;
2 4     4   2248 use strict;
  4         8  
  4         150  
3 4     4   22 use warnings;
  4         7  
  4         109  
4              
5 4     4   21 use Moo;
  4         8  
  4         23  
6             with 'Articulate::Role::Component';
7 4     4   1123 use Articulate::Syntax qw(hash_merge);
  4         8  
  4         33  
8              
9 4     4   1066 use File::Path;
  4         9  
  4         310  
10 4     4   2504 use IO::All;
  4         41987  
  4         36  
11 4     4   716 use YAML;
  4         4733  
  4         232  
12 4     4   24 use Articulate::Syntax;
  4         5  
  4         36  
13 4     4   6201 use Scalar::Util qw(blessed);
  4         7  
  4         190  
14 4     4   443 use FindBin;
  4         715  
  4         7306  
15              
16             =head1 NAME
17              
18             Articulate::Content::Local - store your content locally
19              
20             =cut
21              
22             =head1 DESCRIPTION
23              
24             This content storage interface works by placing content and metadata in a folder structure.
25              
26             For a given location, metadata is stored in C, content in C.
27              
28             Set C in your config to specify where to place the content.
29              
30             Caching is not implemented: get_content_cached simpy calls get_content.
31              
32             =cut
33              
34             =head1 METHODS
35              
36             =cut
37              
38              
39             has content_base => (
40             is => 'rw',
41             lazy => 1, # because depends on app
42             default => sub {
43             my $self = shift;
44             return (
45             undef
46             # $self->framework->appdir
47             // $FindBin::Bin
48             ).'/content/';
49             },
50             coerce => sub {
51             my $content_base = shift;
52             unless (-d $content_base) {
53             File::Path::make_path $content_base;
54             throw_error (Internal => 'Could not initialise content base') unless (-d $content_base);
55             }
56             return $content_base;
57             },
58             );
59              
60             sub ensure_exists { # internal method
61 10     10 0 98 my $self = shift;
62 10   50     32 my $true_location_full = shift // return undef;
63 10         15 my $true_location = $true_location_full;
64 10         112 $true_location =~ s~[^/]+\.[^/]+$~~; #:5.12 doesn't have s///r
65 10 100       203 unless (-d $true_location) {
66 3         1609 File::Path::make_path $true_location;
67             }
68 10 50       163 return -d $true_location ? $true_location_full : throw_error ('Internal' => 'Could not create directory for location');
69             }
70              
71             sub true_location {
72 69     69 0 68 my $self = shift;
73 69         1540 return $self->content_base . shift;
74             }
75              
76              
77             =head3 get_item
78              
79             $storage->get_item( 'zone/public/article/hello-world' )
80              
81             Retrieves the metadata for the content at that location.
82              
83             =cut
84              
85             sub get_item {
86 2     2 1 348 my $self = shift;
87 2         8 my $location = shift->location;
88 2 50       9 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
89 2 50       8 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
90 2         75 my $item = $self->construction->construct( { location => $location } );
91 2         8 $item->meta ( $self->get_meta($item) );
92 2         1147 $item->content ( $self->get_content($item) );
93 2         12 return $item;
94             }
95              
96             =head3 get_meta
97              
98             $storage->get_meta( 'zone/public/article/hello-world' )
99              
100             Retrieves the metadata for the content at that location.
101              
102             =cut
103              
104             sub get_meta {
105 5     5 1 18 my $self = shift;
106 5         5 my $item = shift;
107 5         75 my $location = $item->location;
108 5 50       36 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
109 5 100       29 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
110 3         120 my $fn = $self->true_location ( $location . '/meta.yml' );
111 3 50       87 return YAML::LoadFile($fn) if -e $fn;
112 0         0 return {};
113             }
114              
115             =head3 set_meta
116              
117             $storage->set_meta( 'zone/public/article/hello-world', {...} )
118              
119             Sets the metadata for the content at that location.
120              
121             =cut
122              
123             sub set_meta {
124 2     2 1 82 my $self = shift;
125 2         4 my $item = shift;
126 2         41 my $location = $item->location;
127 2 50       17 throw_error Internal => "Bad location ".$location unless $self->navigation->valid_location( $location );
128 2 100       10 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
129 1         41 my $fn = $self->ensure_exists( $self->true_location( $location . '/meta.yml' ) );
130 1         11 YAML::DumpFile($fn, $item->meta);
131 1         2262 return $item;
132             }
133              
134             =head3 patch_meta
135              
136             $storage->patch_meta( 'zone/public/article/hello-world', {...} )
137              
138             Alters the metadata for the content at that location. Existing keys are retained.
139              
140             CURRENTLY this affects top-level keys only, but a descent algorigthm is planned.
141              
142             =cut
143              
144             sub patch_meta {
145 0     0 1 0 my $self = shift;
146 0         0 my $item = shift;
147 0         0 my $location = $item->location;
148 0 0       0 throw_error Internal => "Bad location ".$location unless $self->navigation->valid_location( $location );
149 0 0       0 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
150 0         0 my $fn = $self->ensure_exists( $self->true_location( $location . '/meta.yml') );
151 0         0 my $old_data = {};
152 0 0       0 $old_data = YAML::LoadFile($fn) if -e $fn;
153 0         0 return YAML::DumpFile($fn, hash_merge($old_data, $item->meta));
154             }
155              
156              
157             =head3 get_settings
158              
159             $storage->get_settings('zone/public/article/hello-world')
160              
161             Retrieves the settings for the content at that location.
162              
163             =cut
164              
165             sub get_settings {
166 0     0 1 0 my $self = shift;
167 0         0 my $item = shift;
168 0         0 my $location = $item->location;
169 0 0       0 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
170 0 0       0 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
171 0         0 my $fn = $self->true_location ( $location . '/settings.yml' );
172 0 0       0 return YAML::LoadFile($fn) if -e $fn;
173 0         0 return {};
174             }
175              
176             =head3 set_settings
177              
178             $storage->set_settings('zone/public/article/hello-world', $amended_settings)
179              
180             Retrieves the settings for the content at that location.
181              
182             =cut
183              
184             sub set_settings {
185 0     0 1 0 my $self = shift;
186 0         0 my $location = shift->location;
187 0         0 my $settings = shift;
188 0 0       0 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
189 0 0       0 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
190 0         0 my $fn = $self->ensure_exists( $self->true_location( $location . '/settings.yml' ) );
191 0         0 YAML::DumpFile($fn, $settings);
192 0         0 return $settings;
193             }
194              
195             =head3 get_settings_complete
196              
197             $storage->get_settings_complete('zone/public/article/hello-world')
198              
199             Retrieves the settings for the content at that location.
200              
201             =cut
202              
203             sub get_settings_complete {
204 0     0 1 0 my $self = shift;
205 0         0 my $location = shift->location;
206 0 0       0 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
207 0         0 my @paths = split /\//, $location;
208 0         0 my $current_path = $self->true_location( '' ).'/';
209 0         0 my $settings = {};
210 0         0 foreach my $p (@paths) {
211 0         0 my $fn = $current_path . 'settings.yml';
212 0         0 my $lvl_settings = {};
213 0 0       0 $lvl_settings = YAML::LoadFile($fn) if -e $fn;
214 0         0 $settings = hash_merge ($settings, $lvl_settings)
215             }
216 0         0 return $settings;
217             }
218              
219              
220             =head3 get_content
221              
222             $storage->get_content('zone/public/article/hello-world')
223              
224             Retrieves the content at that location.
225              
226             =cut
227              
228             sub get_content {
229 10     10 1 30 my $self = shift;
230 10         197 my $location = shift->location;
231 10 50       77 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
232 10 100       36 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
233 8         313 my $fn = $self->true_location( $location . '/content.blob' );
234 8 50       360 open my $fh, '<', $fn or throw_error Internal => "Cannot open file $fn to read";
235 8         197 return '' . (join '', <$fh>);
236             }
237              
238              
239             =head3 set_content
240              
241             $storage->set_content('zone/public/article/hello-world', $blob);
242              
243             Places content at that location.
244              
245             =cut
246              
247              
248             sub _is_upload {
249 5     5   9 my $content = shift;
250 5   33     25 return ( blessed $content and $content->isa('Articulate::File') ); # todo: have this wrapped by an articulate class which interfaces with the FrameworkAdapter
251             }
252              
253             sub _write_data {
254 5     5   10 my ($content, $fn) = @_;
255 5 50       401 open my $fh, '>', $fn or throw_error Internal => "Cannot open file $fn to write";
256 5         49 print $fh $content;
257 5         198 close $fh;
258             }
259              
260             sub _copy_upload {
261 0     0   0 my ($content, $fn) = @_;
262 0         0 my $content_fh = $content->io;
263 0         0 local $/;
264 0 0       0 open my $fh, '>', $fn or throw_error Internal => "Cannot open file $fn to write";
265 0         0 binmode $fh, ':raw';
266 0         0 print $fn while <$content_fh>;
267 0         0 close $fh;
268             }
269              
270             sub _write_content {
271 5     5   11 my ($content, $fn) = @_;
272 5   50     22 $content //= '';
273 5 50       18 if ( _is_upload($content) ) {
274 0         0 _copy_upload( $content, $fn );
275             } else {
276 5         23 _write_data( $content, $fn );
277             }
278             }
279              
280             sub set_content {
281 2     2 1 90 my $self = shift;
282 2         6 my $item = shift;
283 2         43 my $location = $item->location;
284 2 50       83 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
285 2 100       12 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
286 1         46 my $fn = $self->ensure_exists( $self->true_location( $location . '/content.blob' ) );
287 1         8 _write_content( $item->content, $fn );
288 1         8 return $location;
289             }
290              
291             =head3 create_item
292              
293             $storage->create_item('zone/public/article/hello-world', $meta, $blob);
294              
295             Places meta and content at that location.
296              
297             =cut
298              
299              
300             sub create_item {
301 5     5 1 2767 my $self = shift;
302 5         10 my $item = shift;
303 5         119 my $location = $item->location;
304 5 50       41 throw_error Internal => "Bad location ".$location unless $self->navigation->valid_location( $location );
305 5 100       18 throw_error AlreadyExists => "Cannot create: item already exists at ".$location if $self->item_exists($location);
306             {
307 4         123 my $fn = $self->ensure_exists( $self->true_location( $location . '/content.blob' ) );
  4         17  
308 4         31 _write_content( $item->content, $fn );
309             }
310             {
311 4         9 my $fn = $self->ensure_exists( $self->true_location( $location . '/meta.yml' ) );
  4         20  
312 4         31 YAML::DumpFile($fn, $item->meta);
313             }
314 4         21250 $item->content( $self->get_content($item) );
315 4         22 return $item;
316             }
317              
318             =head3 item_exists
319              
320             if ($storage->item_exists( 'zone/public/article/hello-world')) {
321             ...
322             }
323              
324             Determines if the item has been created (only the C is tested).
325              
326             =cut
327              
328              
329             sub item_exists {
330 44     44 1 1974 my $self = shift;
331 44         121 my $location = shift->location;
332 44 50       122 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
333 44         113 return -e $self->true_location( $location . '/meta.yml' );
334             }
335              
336             =head3 list_items
337              
338             $storage->list_items ('/zone/public'); # 'hello-world', 'second-item' )
339              
340             Returns a list of items in the.
341              
342             =cut
343              
344              
345             sub list_items {
346 2     2 1 41 my $self = shift;
347 2         5 my $location = shift->location;
348             # throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location ); # actually, no, because /zone fails but /zone/foo passes
349 2         5 my $true_location = $self->true_location( $location );
350 2         3 my @contents;
351 2 50       35 return @contents unless -d $true_location;
352 2 50       52 opendir (my $dh, $true_location) or throw_error NotFound => ('Could not open '.$true_location);
353 2         51 while (my $fn = readdir $dh) {
354 6         145 my $child_dn = $true_location.'/'.$fn;
355 6 50       70 next unless -d $child_dn;
356 6 100 66     17 push @contents, $fn if $self->navigation->valid_location( $location .'/'.$fn ) and $self->item_exists( loc $location.'/'.$fn );
357             }
358 2         97 return @contents;
359             }
360              
361             sub get_content_cached {
362 2     2 0 89 my $self = shift;
363 2         10 $self->get_content(@_);
364             }
365              
366             sub get_meta_cached {
367 2     2 0 600 my $self = shift;
368 2         18 $self->get_meta(@_);
369             }
370              
371             =head3 empty_all_content
372              
373             $storage->empty_all_content;
374              
375             Removes all content. This is totally irreversible, unless you took a backup!
376              
377             =cut
378              
379             sub empty_all_content {
380 4     4 1 4177 my $self = shift;
381 4         46 my $true_location = $self->content_base;
382              
383 4 50 33     139 throw_error Internal => "Won't empty all content, this looks too dangerous" if (
384             -d "$true_location/.git"
385             or
386             -f "$true_location/Makefile.PL"
387             );
388              
389 4         107 File::Path::remove_tree( $self->content_base, {keep_root => 1} );
390             }
391              
392             =head3 delete_item
393              
394             $storage->delete_item ('/zone/public');
395              
396             Deletes the item and all its descendants.
397              
398             =cut
399              
400             sub delete_item {
401 3     3 1 945 my $self = shift;
402 3         25 my $location = shift->location;
403              
404 3 50       14 throw_error Internal => "Use empty_all_content instead to delete the root" if "$location" eq '/';
405 3 50       13 throw_error Internal => "Bad location $location" unless $self->navigation->valid_location( $location );
406 3 100       14 throw_error NotFound => "No content at $location" unless $self->item_exists($location);
407              
408 2         73 my $true_location = $self->true_location( $location );
409 2         1178 File::Path::remove_tree( $true_location );
410             }
411              
412             =head1 SEE ALSO
413              
414             =over
415              
416             =item * L
417              
418             =item * L
419              
420             =back
421              
422             =cut
423              
424             1;