File Coverage

blib/lib/CatalystX/CRUD/Model/File.pm
Criterion Covered Total %
statement 100 126 79.3
branch 24 46 52.1
condition 7 14 50.0
subroutine 19 22 86.3
pod 11 11 100.0
total 161 219 73.5


line stmt bran cond sub pod time code
1             package CatalystX::CRUD::Model::File;
2 5     5   8439701 use strict;
  5         46  
  5         152  
3 5     5   33 use warnings;
  5         16  
  5         150  
4 5     5   31 use base qw( CatalystX::CRUD::Model );
  5         9  
  5         2900  
5 5     5   57 use File::Find;
  5         17  
  5         464  
6 5     5   44 use Carp;
  5         21  
  5         293  
7 5     5   40 use Data::Dump qw( dump );
  5         12  
  5         248  
8 5     5   38 use Path::Class;
  5         36  
  5         393  
9 5     5   33 use mro 'c3';
  5         11  
  5         146  
10              
11             __PACKAGE__->mk_accessors(qw( inc_path ));
12              
13             our $VERSION = '0.58';
14              
15             # test whether symlink() works at compile time
16             my $SYMLINK_SUPPORTED = eval { symlink( "", "" ); 1 };
17              
18             =head1 NAME
19              
20             CatalystX::CRUD::Model::File - filesystem CRUD model
21              
22             =head1 SYNOPSIS
23              
24             package MyApp::Model::Foo;
25             use base qw( CatalystX::CRUD::Model::File );
26             __PACKAGE__->config(
27             object_class => 'MyApp::File',
28             delegate_class => 'Path::Class::File', # optional
29             inc_path => [ '/some/path', '/other/path' ],
30             );
31            
32             1;
33            
34             =head1 DESCRIPTION
35              
36             CatalystX::CRUD::Model::File is an example implementation
37             of CatalystX::CRUD::Model.
38              
39             =head1 METHODS
40              
41             Only new or overridden methods are documented here.
42              
43             =cut
44              
45             =head2 Xsetup
46              
47             Implements the CXC::Model API.
48             Sets the inc_path() (if not already set)
49             to the C<root> config value.
50              
51             =cut
52              
53             sub Xsetup {
54 8     8 1 30 my ( $self, $c ) = @_;
55 8   50     58 $self->{inc_path} ||= [ $c->config->{root} ];
56 8 50       775 if ( $self->config->{delegate_class} ) {
57             $self->object_class->delegate_class(
58 0         0 $self->config->{delegate_class} );
59             }
60 8         620 $self->next::method($c);
61             }
62              
63             =head2 new_object( file => I<path/to/file> )
64              
65             Return a new CatalystX::CRUD::Object::File object.
66              
67             =cut
68              
69             =head2 fetch( file => I<path/to/file> )
70              
71             Read I<path/to/file> from disk and return a CXCO::File object.
72              
73             I<path/to/file> is assumed to be in C<inc_path>
74              
75             If I<path/to/file> is empty, the
76             CatalystX::CRUD::Object::File object is returned but its content()
77             will be undef. If its parent dir is '.', its dir()
78             will be set to the first item in inc_path().
79              
80             If I<path/to/file> is not found, undef is returned.
81              
82             =cut
83              
84             sub fetch {
85 43     43 1 5375 my $self = shift;
86 43         166 my $file = $self->new_object(@_);
87 42         134 $file = $self->prep_new_object($file);
88 42 100       156 return defined -s $file ? $file : undef;
89             }
90              
91             =head2 prep_new_object( I<file> )
92              
93             Searches inc_path() and calls I<file> read() method
94             if file is found.
95              
96             Also verifies that the delegate() has an absolute path set.
97              
98             Called internally by fetch().
99              
100             Returns I<file>.
101              
102             =cut
103              
104             sub prep_new_object {
105 54     54 1 1598 my $self = shift;
106 54 50       176 my $file = shift or croak "file required";
107              
108             # look through inc_path
109 54         364 for my $dir ( @{ $self->inc_path } ) {
  54         178  
110 54         7155 my $test = $self->object_class->delegate_class->new( $dir, $file );
111              
112 54 100       4369 if ( -s $test ) {
113 30         2084 $file->{delegate} = $test;
114 30         154 $file->read;
115 30         68 last;
116             }
117             }
118              
119             #carp dump $file;
120              
121             # make sure delegate() has absolute path
122             # while file() is relative to inc_path.
123 54 100 100     1894 if ( $file->dir eq '.' or !$file->dir->is_absolute ) {
124             $file->{delegate}
125 24         1932 = $self->object_class->delegate_class->new( $self->inc_path->[0],
126             $file );
127             }
128              
129             #carp dump $file;
130 54         2847 return $file;
131             }
132              
133             =head2 inc_path
134              
135             Returns the include path from config(). The include path is searched
136             by search(), count() and iterator().
137              
138             =cut
139              
140             =head2 make_query
141              
142             Returns a I<wanted> subroutine suitable for File::Find.
143              
144             # TODO regex vs exact match
145            
146             =cut
147              
148             sub make_query {
149 10     10 1 1467 my ($self) = @_;
150             return sub {
151 32     32   62 my ( $root, $dir, $f ) = @_;
152 32 50 33     91 return 0
153             if $dir and $dir =~ m!/\.(svn|git)!;
154 32         808 return 1;
155 10         84 };
156             }
157              
158             =head2 search( I<filter_CODE> )
159              
160             Uses File::Find to search through inc_path() for files.
161             I<filter_CODE> should be a CODE ref matching format returned by make_query().
162             If not set, make_query() is called by default.
163              
164             Returns an array ref of CXCO::File objects.
165              
166             =cut
167              
168             sub _find {
169 20     20   40 my ( $self, $filter_sub, $root ) = @_;
170 20         33 my %files;
171 20         56 my $del_class = $self->object_class->delegate_class;
172             my $find_sub = sub {
173              
174             #warn "File::Find::Dir = $File::Find::dir";
175             #warn "file = $_";
176             #warn "name = $File::Find::name";
177              
178 52     52   12338 my $dir = Path::Class::dir($File::Find::dir);
179 52         2558 my $f = Path::Class::file($File::Find::name);
180 52 100       5043 return if $dir eq $f;
181              
182 32 50       1435 return unless $filter_sub->( $root, $dir, $f );
183              
184             # we want the file path relative to $root
185             # since that is the PK
186 32         124 my $rel = $dir->relative($root);
187 32         4629 $rel =~ s!^\./!!;
188 32         550 my $key = $del_class->new( $rel, $_ );
189              
190             #warn "$key => $f";
191              
192 32 100       2019 $files{$key} = $f if -f $f;
193 20         5871 };
194 20         1091 find(
195             { follow => 1,
196             wanted => $find_sub,
197             },
198             $root
199             );
200 20         2916 return \%files;
201             }
202              
203             sub search {
204 10     10 1 2043 my $self = shift;
205 10   33     38 my $filter_sub = shift || $self->make_query;
206 10         14 my @objects;
207 10         23 for my $root ( @{ $self->inc_path } ) {
  10         42  
208 10         1334 my $files = $self->_find( $filter_sub, $root );
209 10         87 for my $relative ( sort keys %$files ) {
210             my $obj = $self->new_object(
211             file => $relative,
212 6         28 delegate => $files->{$relative}
213             );
214 6         32 $obj->read; # just like fetch()
215 6         45 push @objects, $obj;
216             }
217             }
218 10         60 return \@objects;
219             }
220              
221             =head2 count( I<filter_CODE> )
222              
223             Returns number of files matching I<filter_CODE>. See search for a description
224             of I<filter_CODE>.
225              
226             =cut
227              
228             sub count {
229 10     10 1 2002 my $self = shift;
230 10   33     48 my $filter_sub = shift || $self->make_query;
231 10         22 my $count = 0;
232 10         34 for my $root ( @{ $self->inc_path } ) {
  10         48  
233 10         1558 my $files = $self->_find( $filter_sub, $root );
234 10         56 $count += scalar keys %$files;
235             }
236              
237 10         73 return $count;
238             }
239              
240             =head2 iterator( I<filter_CODE> )
241              
242             Acts same as search() but returns a CatalystX::CRUD::Iterator::File
243             object instead of a simple array ref.
244              
245             =cut
246              
247             sub iterator {
248 0     0 1 0 my $self = shift;
249 0         0 my $files = $self->search(@_);
250 0         0 return CatalystX::CRUD::Iterator::File->new($files);
251             }
252              
253             =head2 iterator_related( I<file>, I<rel_name> )
254              
255             Required method. Acts like iterator() for I<rel_name>.
256              
257             =cut
258              
259             sub iterator_related {
260 0     0 1 0 my $self = shift;
261 0 0       0 my $file = shift or $self->throw_error('file required');
262 0 0       0 my $rel_name = shift or $self->throw_error('rel_name required');
263 0 0       0 if ( $rel_name eq 'dir' ) {
264 0         0 my $files = $self->search(@_);
265 0         0 return CatalystX::CRUD::Iterator::File->new($files);
266             }
267             else {
268 0         0 $self->throw_error("unsupported relationship name: $rel_name");
269             }
270             }
271              
272             =head2 add_related( I<file>, I<rel_name>, I<other_file_name>, I<overwrite> )
273              
274             For I<rel_name> of "dir" will create a symlink for I<other_file_name>'s
275             basename to I<file> in the same directory as I<file>.
276              
277             If a file already exists for I<other_file_name> in the same
278             dir as I<file> will throw an error indicating the relationship
279             already exists. To stop the error being thrown, pass a true
280             value for the I<overwrite> param.
281              
282             If the symlink fails, will throw_error().
283              
284             If symlink() is not supported on your system, will print an error
285             to the Catalyst log.
286              
287             =cut
288              
289             sub add_related {
290 3     3 1 605 my ( $self, $file, $rel_name, $other_file_name, $overwrite ) = @_;
291              
292 3 50       13 if ( !$SYMLINK_SUPPORTED ) {
293 0         0 $self->context->log->error(
294             "symlink() is not supported on this system");
295 0         0 return;
296             }
297              
298 3         11 my $other_file = $self->fetch( file => $other_file_name );
299              
300 3 50       174 unless ( -r $other_file ) {
301 0         0 $self->throw_error("no such file $other_file");
302             }
303              
304 3 50       183 if ( $rel_name eq 'dir' ) {
305              
306             # if in the same dir, already related.
307 3 50       20 if ( $other_file->dir eq $file->dir ) {
308 0 0       0 if ($overwrite) {
309 0         0 return 1; # nothing to do
310             }
311 0         0 $self->throw_error("relationship already exists");
312             }
313              
314             # if not, create symlink
315 3         147 my $link = $self->object_class->delegate_class->new( $file->dir,
316             $other_file->basename );
317 3 50       290 if ( !symlink( "$file", "$link" ) ) {
318 0         0 $self->throw_error("failed to symlink $link => $file: $@");
319             }
320              
321             }
322             else {
323 0         0 $self->throw_error("unsupported relationship name: $rel_name");
324             }
325              
326 3         592 return $other_file;
327             }
328              
329             =head2 put_related( I<file>, I<rel_name>, I<other_file_name> )
330              
331             Calls add_related() with overwrite option.
332              
333             =cut
334              
335             sub put_related {
336 0     0 1 0 my $self = shift;
337 0         0 return $self->add_related( @_, 1 ); # overwrite
338             }
339              
340             =head2 rm_related( I<file>, I<rel_name>, I<other_file_name> )
341              
342             For I<rel_name> of "dir" will create a symlink for I<other_file_name>'s
343             basename to I<file> in the same directory as I<file>.
344              
345             If the symlink() function is not supported, will log an error and return
346             without doing anything.
347              
348             If the symlink represented by I<other_file_name> does not exist
349             or is not a symlink, will throw an error.
350              
351             If the unlink fails will also throw an error.
352              
353             =cut
354              
355             sub rm_related {
356 3     3 1 611 my ( $self, $file, $rel_name, $other_file_name ) = @_;
357              
358 3 50       11 if ( !$SYMLINK_SUPPORTED ) {
359 0         0 $self->context->log->error(
360             "symlink() is not supported on this system");
361 0         0 return;
362             }
363              
364 3         16 my $other_file = $self->fetch( file => $other_file_name );
365              
366 3 50       172 unless ( -r $other_file ) {
367 0         0 $self->throw_error("no such file $other_file : $!");
368             }
369              
370 3 50       176 if ( $rel_name eq 'dir' ) {
371 3         24 my $link = $self->object_class->delegate_class->new( $file->dir,
372             $other_file->basename );
373              
374 3 50       265 unless ( -l $link ) {
375 0         0 $self->throw_error("$other_file is not a symlink");
376             }
377              
378 3 50       201 unlink($link) or $self->throw_error("unlink for $link failed: $!");
379              
380 3         444 return 1;
381              
382             }
383             else {
384 0           $self->throw_error("unsupported relationship name: $rel_name");
385             }
386              
387             }
388              
389             1;
390              
391             __END__
392              
393             =head1 AUTHOR
394              
395             Peter Karman, C<< <perl at peknet.com> >>
396              
397             =head1 BUGS
398              
399             Please report any bugs or feature requests to
400             C<bug-catalystx-crud at rt.cpan.org>, or through the web interface at
401             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CatalystX-CRUD>.
402             I will be notified, and then you'll automatically be notified of progress on
403             your bug as I make changes.
404              
405             =head1 SUPPORT
406              
407             You can find documentation for this module with the perldoc command.
408              
409             perldoc CatalystX::CRUD
410              
411             You can also look for information at:
412              
413             =over 4
414              
415             =item * AnnoCPAN: Annotated CPAN documentation
416              
417             L<http://annocpan.org/dist/CatalystX-CRUD>
418              
419             =item * CPAN Ratings
420              
421             L<http://cpanratings.perl.org/d/CatalystX-CRUD>
422              
423             =item * RT: CPAN's request tracker
424              
425             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=CatalystX-CRUD>
426              
427             =item * Search CPAN
428              
429             L<http://search.cpan.org/dist/CatalystX-CRUD>
430              
431             =back
432              
433             =head1 COPYRIGHT & LICENSE
434              
435             Copyright 2007 Peter Karman, all rights reserved.
436              
437             This program is free software; you can redistribute it and/or modify it
438             under the same terms as Perl itself.
439              
440             =cut