File Coverage

blib/lib/App/MaMGal/Entry/Dir.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             # mamgal - a program for creating static image galleries
2             # Copyright 2007-2010 Marcin Owsiany
3             # See the README file for license information
4             # The directory encapsulating class
5             package App::MaMGal::Entry::Dir;
6 1     1   7 use strict;
  1         1  
  1         45  
7 1     1   6 use warnings;
  1         2  
  1         32  
8 1     1   6 use base 'App::MaMGal::Entry';
  1         2  
  1         701  
9 1     1   6 use Carp;
  1         2  
  1         61  
10 1     1   660 use App::MaMGal::Entry::Picture;
  1         2  
  1         27  
11 1     1   706 use App::MaMGal::DirIcon;
  1         3  
  1         29  
12 1     1   794 use Image::Magick;
  0            
  0            
13             use App::MaMGal::Exceptions;
14              
15             sub child { $_[0]->{path_name}.'/'.$_[1] }
16             sub page_path { $_[0]->{base_name}.'/index.html' }
17             sub thumbnail_path { $_[0]->{base_name}.'/.mamgal-index.png' }
18              
19             sub init
20             {
21             my $self = shift;
22             $self->SUPER::init(@_);
23             if ($self->{dir_name} eq '/' and ($self->{base_name} eq '/' or $self->{base_name} eq '.')) {
24             $self->{path_name} = '/';
25             $self->{base_name} = '/';
26             $self->{is_root} = 1;
27             } elsif (-e $self->child('.mamgal-root')) {
28             $self->{is_root} = 1;
29             }
30             }
31              
32             sub set_root
33             {
34             my $self = shift;
35             my $was_root = $self->is_root;
36             my $is_root = $self->{is_root} = shift;
37              
38             return if $is_root == $was_root;
39              
40             if ($is_root) {
41             $self->_write_contents_to(sub {''}, '.mamgal-root');
42             } else {
43             unlink($self->child('.mamgal-root')) or App::MaMGal::SystemException->throw(message => '%s: unlink failed: %s', objects => [$self->child(".mamgal-root"), $!]);
44             }
45             }
46              
47             sub is_root
48             {
49             my $self = shift;
50             return $self->{is_root} || 0;
51             }
52              
53             sub make
54             {
55             my $self = shift;
56             my $tools = $self->tools or croak "Tools were not injected";
57             my $formatter = $tools->{formatter} or croak "Formatter required";
58             ref $formatter and $formatter->isa('App::MaMGal::Formatter') or croak "[$formatter] is not a formatter";
59              
60             my $deleted_css = $self->_write_stylesheet;
61             # Force a rewrite of all html pages if the stylesheet was removed, as they now need to point to a different place
62             my @active_files = map { $_->make(force_slide => $deleted_css) } $self->elements;
63             my $pruned_count = $self->_prune_inactive_files(\@active_files);
64             $self->_write_montage($pruned_count);
65             $self->_write_index($deleted_css or $pruned_count);
66             return ()
67             }
68              
69             sub _write_index
70             {
71             my $self = shift;
72             my $force = shift;
73             my $formatter = $self->tools->{formatter};
74             $self->_write_contents_to(sub { $formatter->format($self) }, 'index.html') unless ($self->fresher_than_me($self->child('index.html')) and not $force);
75             }
76              
77             sub _write_stylesheet
78             {
79             my $self = shift;
80             if ($self->is_root) {
81             my $formatter = $self->tools->{formatter};
82             $self->_write_contents_to(sub { $formatter->stylesheet }, '.mamgal-style.css');
83             } else {
84             # Delete legacy stylesheet files in directories other than root.
85             # TODO: remove this code a few releases from 1.2
86             my $path = $self->child('.mamgal-style.css');
87             return unless -e $path;
88             unlink($path) or App::MaMGal::SystemException->throw(message => '%s: unlink failed: %s', objects => [$path, $!]);
89             return 1;
90             }
91             }
92              
93             sub ensure_subdir_exists
94             {
95             my $self = shift;
96             my $basename = shift;
97             my $dir = $self->child($basename);
98             mkdir $dir or App::MaMGal::SystemException->throw(message => '%s: mkdir failed: %s', objects => [$dir, $!]) unless -w $dir;
99             }
100              
101             # get _picture_ neighbours of given picture
102             sub neighbours_of_index
103             {
104             my $self = shift;
105             my $idx = shift;
106             croak "neighbours_of_index must run in array context" unless wantarray;
107             my @elements = $self->elements;
108             $idx >= 0 or croak "Pic index must be at least 0";
109             $idx < scalar @elements or croak "Pic index out of bounds for this dir";
110              
111             my ($prev, $next);
112             my $i = $idx - 1;
113             while ($i >= 0) {
114             $prev = $elements[$i], last if $elements[$i]->isa('App::MaMGal::Entry::Picture');
115             $i--;
116             }
117             $i = $idx + 1;
118             while ($i < scalar @elements) {
119             $next = $elements[$i], last if $elements[$i]->isa('App::MaMGal::Entry::Picture');
120             $i++;
121             }
122             return $prev, $next;
123             }
124              
125              
126             sub _write_contents_to
127             {
128             my $self = shift;
129             my $code = shift;
130             my $suffix = shift;
131             # TODO: this will be an issue when mamgal goes multi-threaded
132             my $tmp_name = $self->child('.mamgal-tmp');
133             my $full_name = $self->child($suffix);
134             $self->SUPER::_write_contents_to($code, $tmp_name, $full_name);
135             }
136              
137             sub _side_length
138             {
139             my $self = shift;
140             my $picture_count = shift;
141              
142             # The montage is a visual clue that the object is a container.
143             # Therefore ensure we do not get a 1x1 montage, because it would be
144             # indistinguishable from a single image.
145             my $sqrt = sqrt($picture_count);
146             my $int = int($sqrt);
147             my $side = $int == $sqrt ? $int : $int + 1;
148             $side = 2 if $side < 2;
149             return $side;
150             }
151              
152             sub _write_montage
153             {
154             my $self = shift;
155             my $pruned_files = shift;
156              
157             my @images = $self->_all_interesting_elements;
158              
159             unless (@images) {
160             $self->_write_contents_to(sub { App::MaMGal::DirIcon->img }, '.mamgal-index.png');
161             return;
162             }
163              
164             my $montage_path = $self->child('.mamgal-index.png');
165             # Return early if the montage is fresh
166             return if $self->fresher_than_me($montage_path, consider_interesting_only => 1) and $pruned_files == 0;
167              
168             # Get just a bunch of images, not all of them.
169             my $montage_count = scalar @images > 36 ? 36 : scalar @images;
170             # Stack them all together
171             my $stack = Image::Magick->new;
172             push @$stack, map {
173             my $img = Image::Magick->new;
174             my $rr;
175             $rr = $img->Read($_->tile_path) and App::MaMGal::SystemException->throw(message => '%s: %s', objects => [$_->tile_path, $rr]);
176             $img } @images[0..($montage_count-1)];
177              
178             my $side = $self->_side_length($montage_count);
179              
180             my ($m_x, $m_y) = (200, 150);
181              
182             my ($montage, $r);
183             # Do the magick, scale and write.
184             $r = $montage = $stack->Montage(tile => $side.'x'.$side, geometry => $m_x.'x'.$m_y, border => 2);
185             ref($r) or App::MaMGal::SystemException->throw(message => '%s: montage failed: %s', objects => [$montage_path, $r]);
186             $r = App::MaMGal::Entry::Picture->scale_into($montage, $m_x, $m_y) and App::MaMGal::SystemException->throw(message => '%s: scaling failed: %s', objects => [$montage_path, $r]);
187             $r = $montage->Write($montage_path) and App::MaMGal::SystemException->throw(message => '%s: writing montage failed: %s', objects => [$montage_path, $r]);
188             }
189              
190             sub _ignorable_name($)
191             {
192             my $self = shift;
193             my $name = shift;
194             # ignore hidden files
195             return 1 if substr($_, 0, 1) eq '.';
196             # TODO: optimize out contants calls, keeping in mind that they are not really constant (eg. tests change them when testing slides/miniatures generation)
197             return 1 if grep { $_ eq $name } (qw(lost+found index.html .mamgal-index.png .mamgal-style.css), $self->slides_dir, $self->thumbnails_dir, $self->medium_dir);
198             return 0;
199             }
200              
201             sub _prune_inactive_files
202             {
203             my $self = shift;
204             my $active_files = shift;
205             # delete old temporary file, if any
206             unlink $self->child('.mamgal-tmp') if (-e $self->child('.mamgal-tmp'));
207             my @known_subdirs = ($self->slides_dir, $self->thumbnails_dir, $self->medium_dir);
208             # first, sanity check so we know if we start creating files outside the known subdirs
209             foreach my $f (@$active_files) {
210             confess "internal error: [$f] has an unknown prefix" unless
211             substr($f, 0, length($known_subdirs[0]) + 1) eq $known_subdirs[0].'/' or
212             substr($f, 0, length($known_subdirs[1]) + 1) eq $known_subdirs[1].'/' or
213             substr($f, 0, length($known_subdirs[2]) + 1) eq $known_subdirs[2].'/';
214             }
215             my %active = map { $_ => 1 } @$active_files;
216             my $base = $self->{path_name};
217             my $pruned_count = 0;
218             foreach my $dir (@known_subdirs) {
219             # If the directory is not there, we have nothing to do about it
220             next unless -d $base.'/'.$dir;
221             # Read the names from the dir
222             opendir DIR, $base.'/'.$dir or App::MaMGal::SystemException->throw(message => '%s: opendir failed: %s', objects => ["$base/$dir", $!]);
223             my @entries = grep { $_ ne '.' and $_ ne '..' } readdir DIR;
224             closedir DIR or App::MaMGal::SystemException->throw(message => '%s: closedir failed: %s', objects => ["$base/$dir", $!]);
225             # Delete the files which are not "active"
226             my $at_start = scalar @entries;
227             my $deleted = 0;
228             foreach my $entry (@entries) {
229             next if $active{$dir.'/'.$entry};
230             # Before unlinking, update touch self so that if we crash between unlinking and updating thumbnail, it will be done on subsequent invocation.
231             utime(undef, undef, $base);
232             unlink($base.'/'.$dir.'/'.$entry) or App::MaMGal::SystemException->throw(message => '%s: unlink failed: %s', objects => ["$base/$dir/$entry", $!]);
233             $deleted++;
234             }
235             $pruned_count += $deleted;
236             rmdir($base.'/'.$dir) or App::MaMGal::SystemException->throw(message => '%s: rmdir failed: %s', objects => ["$base/$dir", $!]) if $at_start == $deleted;
237             }
238             return $pruned_count;
239             }
240              
241             sub elements
242             {
243             my $self = shift;
244             # Lookup the cache
245             return @{$self->{elements}} if exists $self->{elements};
246              
247             # Get entry factory
248             my $tools = $self->tools or croak "Tools were not injected";
249             my $entry_factory = $tools->{entry_factory} or croak "Entry factory required";
250             ref $entry_factory and $entry_factory->isa('App::MaMGal::EntryFactory') or croak "[$entry_factory] is not an entry factory";
251              
252             # Read the names from the dir
253             my $path = $self->{path_name};
254             opendir DIR, $path or App::MaMGal::SystemException->throw(message => '%s: opendir failed: %s', objects => [$path, $!]);
255             my @entries = sort { $a cmp $b } grep { ! $self->_ignorable_name($_) } readdir DIR;
256             closedir DIR or App::MaMGal::SystemException->throw(message => '%s: closedir failed: %s', objects => [$path, $!]);
257              
258             my $i = 0;
259             # Instantiate objects and cache them
260             $self->{elements} = [ map {
261             $_ = $path.'/'.$_ ;
262             my $e = $entry_factory->create_entry_for($_);
263             $e->set_element_index($i++);
264             $e->set_container($self);
265             $e
266             } @entries
267             ];
268             return @{$self->{elements}};
269             }
270              
271             sub containers
272             {
273             my $self = shift;
274             return if $self->is_root;
275             return $self->SUPER::containers(@_);
276             }
277              
278             sub creation_time
279             {
280             my $self = shift;
281              
282             my $spaces = join('', map { " " } $self->containers);
283              
284             my @elements = $self->elements;
285             if (scalar @elements == 1) {
286             return $elements[0]->creation_time;
287             } elsif (scalar @elements > 1) {
288             # Lookup cache
289             return wantarray ? @{$self->{cct}} : $self->{cct}->[1] if exists $self->{cct};
290             my ($oldest, $youngest) = (undef, undef);
291             foreach my $t (map { $_->creation_time } @elements) {
292             $oldest = $t if not defined $oldest or $oldest > $t;
293             $youngest = $t if not defined $youngest or $youngest < $t;
294             }
295             $self->{cct} = [$oldest, $youngest];
296             return ($oldest, $youngest) if wantarray;
297             return $youngest;
298             }
299             return $self->SUPER::creation_time;
300             }
301              
302             # Returns the most recent of:
303             # - this directory inode's modification time
304             # - all of interesting elements' content modification time
305             sub content_modification_time
306             {
307             my $self = shift;
308             my %opts = @_;
309             my $own = $self->SUPER::content_modification_time;
310             return $own if $opts{only_own};
311             my @elements;
312             if ($opts{consider_interesting_only}) {
313             @elements = $self->_all_interesting_elements;
314             } else {
315             @elements = $self->elements;
316             }
317             foreach my $i (@elements) {
318             # Prevent doing a deep tree walk
319             my $that = $i->content_modification_time(only_own => 1);
320             $own = $that if $that > $own;
321             }
322             return $own;
323             }
324              
325             sub is_interesting
326             {
327             my $self = shift;
328             defined $self->_first_interesting_element ? 1 : 0
329             }
330              
331             sub tile_path
332             {
333             my $self = shift;
334             # TODO: choice of the first element is arbitrary, there might be a better heuristic
335             $self->_first_interesting_element->tile_path
336             }
337              
338             sub _first_interesting_element
339             {
340             my $self = shift;
341             foreach ($self->elements) { return $_ if $_->is_interesting }
342             return undef;
343             }
344              
345             sub _all_interesting_elements
346             {
347             my $self = shift;
348             grep { $_->is_interesting } $self->elements
349             }
350              
351             1;