File Coverage

blib/lib/Statocles/Command.pm
Criterion Covered Total %
statement 259 331 78.2
branch 93 134 69.4
condition 25 47 53.1
subroutine 15 20 75.0
pod 1 3 33.3
total 393 535 73.4


line stmt bran cond sub pod time code
1             package Statocles::Command;
2             our $VERSION = '0.084';
3             # ABSTRACT: The statocles command-line interface
4              
5 7     7   161417 use Statocles::Base 'Class';
  7         18  
  7         58  
6 7     7   48206 use Scalar::Util qw( blessed );
  7         18  
  7         509  
7 7     7   48 use Getopt::Long qw( GetOptionsFromArray :config pass_through bundling no_auto_abbrev );
  7         16  
  7         77  
8 7     7   4183 use Pod::Usage::Return qw( pod2usage );
  7         2627  
  7         372  
9 7     7   1532 use File::Share qw( dist_dir );
  7         3295  
  7         318  
10 7     7   2656 use Beam::Wire;
  7         2039780  
  7         339  
11 7     7   2589 use Statocles::Template;
  7         31  
  7         18986  
12              
13             my @VERBOSE = ( "warn", "info", "debug", "trace" );
14              
15             #pod =attr site
16             #pod
17             #pod The L<site|Statocles::Site> we're working with.
18             #pod
19             #pod =cut
20              
21             has site => (
22             is => 'ro',
23             isa => InstanceOf['Statocles::Site'],
24             );
25              
26             #pod =attr log
27             #pod
28             #pod A L<Mojo::Log> object for logging. Defaults to the current site's C<log> attribute.
29             #pod
30             #pod =cut
31              
32             has log => (
33             is => 'rw',
34             lazy => 1,
35             default => sub {
36             $_[0]->site->log;
37             },
38             );
39              
40             #pod =method main
41             #pod
42             #pod my $exitval = $cmd->main( @argv );
43             #pod
44             #pod Run the command given in @argv. See L<statocles> for a list of commands and
45             #pod options.
46             #pod
47             #pod =cut
48              
49             sub main {
50 51     51 1 1398031 my ( $class, @argv ) = @_;
51              
52 51         393 my %opt = (
53             config => 'site.yml',
54             site => 'site',
55             verbose => 0,
56             );
57 51         446 GetOptionsFromArray( \@argv, \%opt,
58             'port|p=i',
59             'config:s',
60             'site:s',
61             'help|h',
62             'version',
63             'verbose|v+',
64             );
65 51 100       30689 return pod2usage(0) if $opt{help};
66              
67 49 100 100     451 if ( $opt{version} || ( $opt{verbose} && !@argv ) ) {
      100        
68 2         134 say "Statocles version $Statocles::Command::VERSION (Perl $^V)";
69 2         15 return 0;
70             }
71              
72 47         133 my $method = shift @argv;
73 47 100       154 return pod2usage("ERROR: Missing command") unless $method;
74              
75             # Create site does not require a config file
76 46 100       174 if ( $method eq 'create' ) {
77             # Create a custom logger
78 7         86 my $log = Mojo::Log->new;
79 7         323 $log->handle( \*STDOUT );
80 7         91 $log->level( $VERBOSE[ $opt{verbose} ] );
81 7         215 return $class->new( log => $log )->create_site( \@argv, \%opt );
82             }
83              
84 39 100       608 if ( !-e $opt{config} ) {
85 2         72 warn sprintf qq{ERROR: Could not find config file "\%s"\n}, $opt{config};
86 2         13 return 1;
87             }
88              
89 37         99 my $wire = eval { Beam::Wire->new( file => $opt{config} ) };
  37         1089  
90              
91 37 100       1015145 if ( $@ ) {
92 4 50 33     541 if ( blessed $@ && $@->isa( 'Beam::Wire::Exception::Config' ) ) {
93 4         11 my $remedy;
94 4 100 66     13 if ( $@ =~ /found character that cannot start any token/ || $@ =~ /YAML_PARSE_ERR_NONSPACE_INDENTATION/ ) {
    50 33        
    0 0        
95 2         90 $remedy = "Check that you are not using tabs for indentation. ";
96             }
97             elsif ( $@ =~ /did not find expected key/ || $@ =~ /YAML_PARSE_ERR_INCONSISTENT_INDENTATION/ ) {
98 2         168 $remedy = "Check your indentation. ";
99             }
100             elsif ( $@ =~ /Syck parser/ && $@ =~ /syntax error/ ) {
101 0         0 $remedy = "Check your indentation. ";
102             }
103              
104 4 100       24 my $more_info = ( !$opt{verbose} ? qq{run with the "--verbose" option or } : "" )
105             . "check Statocles::Help::Error";
106              
107             warn sprintf qq{ERROR: Could not load config file "%s". %sFor more information, %s.%s},
108             $opt{config},
109             $remedy,
110             $more_info,
111 4 100       169 ( $opt{verbose} ? "\n\nRaw error: $@" : "" )
112             ;
113              
114 4         195 return 1;
115             }
116 0         0 die $@;
117             }
118              
119 33         105 my $site = eval { $wire->get( $opt{site} ) };
  33         271  
120              
121 33 100       51086 if ( $@ ) {
122 4 100 66     386 if ( blessed $@ && $@->isa( 'Beam::Wire::Exception::NotFound' ) && $@->name eq $opt{site} ) {
      100        
123             warn sprintf qq{ERROR: Could not find site named "%s" in config file "%s"\n},
124 2         149 $opt{site}, $opt{config};
125 2         42 return 1;
126             }
127             warn sprintf qq{ERROR: Could not create site object "%s" in config file "%s": %s\n},
128 2         59 $opt{site}, $opt{config}, $@;
129 2         112 return 1;
130             }
131              
132 29         593 my $cmd = $class->new( site => $site );
133              
134 29 100       8169 if ( $opt{verbose} ) {
135 4         79 $cmd->site->log->handle( \*STDOUT );
136 4         374 $cmd->site->log->level( $VERBOSE[ $opt{verbose} ] );
137             }
138              
139 29 100       258 if ( $method eq 'build' ) {
    100          
    100          
    50          
    100          
    100          
140 11         25 my %build_opt;
141 11         84 GetOptionsFromArray( \@argv, \%build_opt,
142             'date|d=s',
143             );
144 11         2185 $cmd->site->build( %build_opt );
145 11         620 return 0;
146             }
147             elsif ( $method eq 'deploy' ) {
148 7         21 my %deploy_opt;
149 7         61 GetOptionsFromArray( \@argv, \%deploy_opt,
150             'date|d=s',
151             'clean',
152             'message|m=s',
153             );
154 7         2668 $cmd->site->deploy( %deploy_opt );
155              
156 7         14841 return 0;
157             }
158             elsif ( $method eq 'apps' ) {
159 1         7 my $apps = $cmd->site->apps;
160 1         3 for my $app_name ( keys %{ $apps } ) {
  1         6  
161 2         6 my $app = $apps->{$app_name};
162 2         10 my $root = $app->url_root;
163 2         4 my $class = ref $app;
164 2         90 say "$app_name ($root -- $class)";
165             }
166 1         68 return 0;
167             }
168             elsif ( $method eq 'status' ) {
169 0         0 my $status = $cmd->site->_get_status;
170 0 0       0 if ($status->{last_deploy_date}) {
171             say "Last deployed on " .
172             DateTime::Moonpig->from_epoch(
173             epoch => $status->{last_deploy_date},
174 0         0 )->strftime("%Y-%m-%d at %H:%M");
175             say "Deployed up to date " .
176 0   0     0 ( $status->{last_deploy_args}{date} || '-' );
177             } else {
178 0         0 say "Never been deployed";
179             }
180 0         0 return 0;
181             }
182             elsif ( $method eq 'daemon' ) {
183             # Build the site first no matter what. We may end up watching for
184             # future changes, but assume they meant to build first
185 3         6 my %build_opt;
186 3         20 GetOptionsFromArray( \@argv, \%build_opt,
187             'date|d=s',
188             );
189              
190 3         577 require Mojo::Server::Daemon;
191 3         48 my $app = Statocles::Command::_MOJOAPP->new(
192             site => $cmd->site,
193             options => \%build_opt,
194             );
195 3         927 our $daemon = Mojo::Server::Daemon->new(
196             silent => 1,
197             app => $app,
198             );
199              
200 3 100       510 if ( $opt{port} ) {
201 1         7 $daemon->listen([ "http://*:$opt{port}" ]);
202             }
203              
204             # Using start() instead of run() so we can stop() inside the tests
205 3         20 $daemon->start;
206              
207             # Find the port we're listening on
208 3         4303 my $id = $daemon->acceptors->[0];
209 3         20 my $handle = $daemon->ioloop->acceptor( $id )->handle;
210 3   50     43 say "Listening on " . sprintf( 'http://%s:%d', $handle->sockhost || '127.0.0.1', $handle->sockport );
211              
212             # Give control to the IOLoop
213 3         297 Mojo::IOLoop->start;
214             }
215             elsif ( $method eq 'bundle' ) {
216 4         12 my $what = $argv[0];
217 4 50       22 if ( $what eq 'theme' ) {
218 4         8 my $theme_name = $argv[1];
219 4 100       12 if ( !$theme_name ) {
220 1         102 say STDERR "ERROR: No theme name!";
221 1         8 say STDERR "\nUsage:\n\tstatocles bundle theme <name>";
222 1         27 return 1;
223             }
224              
225 3         22 my $dest_dir = $site->theme->store->path;
226 3         22 $cmd->bundle_theme( $theme_name, $dest_dir, @argv[2..$#argv] );
227 3         1017 say qq{Theme "$theme_name" written to "$dest_dir"};
228             }
229             }
230             else {
231 3         9 my $app_name = $method;
232 3         19 my $app = $cmd->site->apps->{ $app_name };
233              
234 3 100       32 if ( !$app ) {
    100          
235 1         8 return pod2usage("ERROR: Unknown command or app '$app_name'");
236             }
237             elsif ( !$app->can( 'command' ) ) {
238 1         83 say STDERR sprintf 'ERROR: Application "%s" has no commands', $app_name;
239 1         38 return 1;
240             }
241              
242 1         22 return $cmd->site->apps->{ $app_name }->command( $app_name, @argv );
243             }
244              
245 6         1203 return 0;
246             }
247              
248             sub create_site {
249 7     7 0 2004 my ( $self, $argv, $opt ) = @_;
250              
251 7         14 my %answer;
252 7         21 my $site_root = '.';
253              
254             # Allow the user to set the base URL and the site folder as an argument
255 7 100       36 if ( @$argv ) {
256 5         14 my $base = $argv->[0];
257 5 100       36 if ( $base =~ m{^https?://(.+)} ) {
258 3         13 $answer{base_url} = $base;
259 3   66     21 $site_root = $argv->[1] || $1;
260             }
261             else {
262 2         10 $answer{base_url} = "http://$base";
263 2   33     18 $site_root = $argv->[1] || $base;
264             }
265             }
266              
267 7         53 my $create_dir = Path::Tiny->new( dist_dir( 'Statocles' ) )->child( 'create' );
268 7         1623 my $question = YAML::Load( $create_dir->child( 'script.yml' )->slurp_utf8 );
269 7         95737 my %prompt = (
270             flavor => 'Which flavor of site would you like? ([1], 2, 0)',
271             bundle_theme => 'Do you want to bundle the theme? ([Y]/n)',
272             base_url => 'What is the URL where the site will be deployed?',
273             deploy_class => 'How would you like to deploy? ([1], 2, 0)',
274             git_branch => 'What branch? [master]',
275             deploy_path => 'Where to deploy the site? (default: current directory)',
276             );
277              
278 7         480 print "\n", $question->{flavor};
279 7         66 print "\n", "\n", $prompt{flavor}, " ";
280 7         267 chomp( $answer{flavor} = <STDIN> );
281 7         151 until ( $answer{flavor} =~ /^[120]*$/ ) {
282 0         0 print $prompt{flavor}, " ";
283 0         0 chomp( $answer{flavor} = <STDIN> );
284             }
285 7 50       45 $answer{flavor} = 1 if $answer{flavor} eq '';
286              
287 7         75 print "\n", "\n", $question->{bundle_theme};
288 7         44 print "\n", "\n", $prompt{bundle_theme}, " ";
289 7         32 chomp( $answer{bundle_theme} = <STDIN> );
290 7         52 until ( $answer{bundle_theme} =~ /^[yn]*$/i ) {
291 0         0 print $prompt{bundle_theme}, " ";
292 0         0 chomp( $answer{bundle_theme} = <STDIN> );
293             }
294 7 100       39 $answer{bundle_theme} = "y" if $answer{bundle_theme} eq '';
295              
296 7 100       30 if ( !$answer{base_url} ) {
297 2         16 print "\n", "\n", $question->{base_url};
298 2         11 print "\n", "\n", $prompt{base_url}, " ";
299 2         9 chomp( $answer{base_url} = <STDIN> );
300 2 100       18 if ( $answer{base_url} !~ m{^https?://} ) {
301 1         6 $answer{base_url} = "http://$answer{base_url}";
302             }
303             }
304              
305 7         52 print "\n", "\n", $question->{deploy_class};
306 7         45 print "\n", "\n", $prompt{deploy_class}, " ";
307 7         35 chomp( $answer{deploy_class} = <STDIN> );
308 7         51 until ( $answer{deploy_class} =~ /^[120]*$/i ) {
309 0         0 print $prompt{deploy_class}, " ";
310 0         0 chomp( $answer{deploy_class} = <STDIN> );
311             }
312 7 100       33 $answer{deploy_class} = 1 if $answer{deploy_class} eq '';
313              
314 7 100       44 if ( $answer{deploy_class} == 1 ) {
    100          
315             # Git deploy questions
316 2         23 print "\n", "\n", $question->{git_branch};
317 2         14 print "\n", "\n", $prompt{git_branch}, " ";
318 2         11 chomp( $answer{git_branch} = <STDIN> );
319 2   50     23 $answer{git_branch} ||= "master";
320             }
321             elsif ( $answer{deploy_class} == 2 ) {
322             # File deploy questions
323 3         21 print "\n", "\n", $question->{deploy_path};
324 3         16 print "\n", "\n", $prompt{deploy_path}, " ";
325 3         13 chomp( $answer{deploy_path} = <STDIN> );
326 3   50     10 $answer{deploy_path} ||= '.';
327             }
328              
329             ### Build the site
330 7         52 my $cwd = cwd;
331 7         394 my $root = Path::Tiny->new( $site_root );
332 7         188 $root->mkpath;
333 7         1131 my $config_tmpl = Statocles::Template->new(
334             path => $create_dir->child( 'site.yml' ),
335             );
336 7         384 my %vars;
337              
338 7 100       43 if ( $answer{flavor} == 1 ) {
    100          
339 2         11 $vars{site}{index} = "/blog";
340 2         20 $vars{site}{nav}{main}[0] = {
341             href => "/",
342             text => "Blog",
343             };
344             }
345             elsif ( $answer{flavor} == 2 ) {
346 3         15 $vars{site}{index} = "/page";
347 3         22 $vars{site}{nav}{main}[0] = {
348             href => "/blog",
349             text => "Blog",
350             };
351             }
352             else {
353 2         10 $vars{site}{index} = "/blog";
354 2         13 $vars{site}{nav}{main}[0] = {
355             href => "/",
356             text => "Blog",
357             };
358             }
359              
360 7 100       43 if ( lc $answer{bundle_theme} eq 'y' ) {
361 2         10 chdir $root;
362 2         36 $self->bundle_theme( 'default', 'theme' );
363 2         679 chdir $cwd;
364 2         38 $vars{theme}{args}{store} = 'theme';
365             }
366             else {
367 5         27 $vars{theme}{args}{store} = '::default';
368             }
369              
370 7 50       28 if ( $answer{base_url} ) {
371 7         23 $vars{site}{base_url} = $answer{base_url};
372             }
373              
374 7 100       36 if ( $answer{deploy_class} == 1 ) {
    100          
375 2         10 $vars{deploy}{class} = 'Statocles::Deploy::Git';
376 2         13 $vars{deploy}{args}{branch} = $answer{git_branch};
377              
378             # Create the git repo
379 2         18 require Git::Repository;
380             # Running init more than once is apparently completely safe, so we don't
381             # even have to check before we run it
382 2         6 chdir $root;
383 2         32 Git::Repository->run( 'init' );
384 2         57314 chdir $cwd;
385 2         53 $root->child( '.gitignore' )->append( "\n.statocles\n" );
386             }
387             elsif ( $answer{deploy_class} == 2 ) {
388 3         12 $vars{deploy}{class} = 'Statocles::Deploy::File';
389 3         11 $vars{deploy}{args}{path} = $answer{deploy_path};
390             }
391             else {
392             # We need a deploy in order to create a Site object
393 2         6 $vars{deploy}{class} = 'Statocles::Deploy::File';
394 2         7 $vars{deploy}{args}{path} = '.';
395             }
396              
397 7         784 $root->child( 'site.yml' )->spew_utf8( $config_tmpl->render( %vars ) );
398 7         2949 my ( $site ) = YAML::Load( $root->child( 'site.yml' )->slurp_utf8 );
399              
400             # Make required store directories
401 7         142463 for my $app ( map { $_->{'$ref'} } values %{ $site->{site}{args}{apps} } ) {
  21         78  
  7         90  
402 21         3031 my $path = $site->{$app}{args}{store};
403 21 50       67 next unless $path;
404 21         82 $root->child( $path )->mkpath;
405             }
406              
407             ### Copy initial site content
408             # Blog
409 7 50       876 if ( my $ref = $site->{site}{args}{apps}{blog} ) {
410 7         39 my $path = $site->{ $ref->{ '$ref' } }{args}{store};
411 7 50       19 if ( $path ) {
412 7         264 my ( undef, undef, undef, $day, $mon, $year ) = localtime;
413 7         28 $year += 1900;
414 7         15 $mon += 1;
415              
416 7         79 my @date_parts = (
417             sprintf( '%04i', $year ),
418             sprintf( '%02i', $mon ),
419             sprintf( '%02i', $day ),
420             );
421              
422 7         30 my $post_path = $root->child( $path, @date_parts, 'first-post', 'index.markdown' );
423 7         304 $post_path->parent->mkpath;
424 7         2235 $create_dir->child( 'blog', 'post.markdown' )->copy( $post_path );
425             }
426             }
427              
428             # Page
429 7 50       3772 if ( my $ref = $site->{site}{args}{apps}{page} ) {
430 7         36 my $path = $site->{ $ref->{ '$ref' } }{args}{store};
431 7 50       29 if ( $path ) {
432 7         30 my $page_path = $root->child( $path, 'index.markdown' );
433 7         326 $page_path->parent->mkpath;
434 7         682 $create_dir->child( 'page', 'index.markdown' )->copy( $page_path );
435             };
436             }
437              
438             ### DONE!
439 7         2272 print "\n", "\n", $question->{finish}, "\n", "\n";
440              
441 7         736 return 0;
442             }
443              
444             sub bundle_theme {
445 5     5 0 27 my ( $self, $name, $dir, @files ) = @_;
446 5         34 my $theme_dest = Path::Tiny->new( $dir );
447 5         154 my $theme_root = Path::Tiny->new( dist_dir( 'Statocles' ), 'theme', $name );
448              
449 5 100       996 if ( !@files ) {
450 4         42 my $iter = $theme_root->iterator({ recurse => 1 });
451 4         179 while ( my $path = $iter->() ) {
452 116 100       9241 next unless $path->is_file;
453 88         961 my $relative = $path->relative( $theme_root );
454 88         14459 push @files, $relative;
455             }
456             }
457             else {
458 1         4 @files = map { Path::Tiny->new( $_ ) } @files;
  4         89  
459             }
460              
461 5         201 for my $path ( @files ) {
462 92         26710 my $abs_path = $path->absolute( $theme_root );
463 92         7665 my $dest = $theme_dest->child( $path );
464             # Don't overwrite site-customized hooks
465 92 100 100     3132 next if ( $abs_path->stat->size == 0 && $dest->exists );
466 87         26313 $self->log->debug( sprintf 'Copying theme file "%s" to "%s"', $path, $dest );
467 87 100       3694 $dest->remove if $dest->exists;
468 87         2079 $dest->parent->mkpath;
469 87         10335 $abs_path->copy( $dest );
470             }
471             }
472              
473             {
474             package # Do not index this
475             Statocles::Command::_MOJOAPP;
476              
477             # Currently, as of Mojolicious 5.12, loading the Mojolicious module here
478             # will load the Mojolicious::Commands module, which calls GetOptions, which
479             # will remove -h, --help, -m, and -s from @ARGV. We fix this by copying
480             # @ARGV in bin/statocles before we call Statocles::Command.
481             #
482             # We could fix this in the future by moving this out into its own module,
483             # that is only loaded after we are done passing @ARGV into main(), above.
484 7     7   71 use Mojo::Base 'Mojolicious';
  7         14  
  7         58  
485 7     7   1131803 use Scalar::Util qw( weaken );
  7         16  
  7         373  
486 7     7   42 use File::Share qw( dist_dir );
  7         15  
  7         7634  
487             has 'site';
488             has options => sub { {} };
489             has cleanup => sub { Mojo::Collection->new };
490              
491             sub DESTROY {
492 2     2   350 my ( $self, $in_global_destruction ) = @_;
493 2 50       9 return unless $self->cleanup;
494 2     0   21 $self->cleanup->each( sub { $_->() } );
  0         0  
495             }
496              
497             sub startup {
498 3     3   12453 my ( $self ) = @_;
499 3         13 $self->log( $self->site->log );
500              
501             # First build the site
502 3         165 $self->site->build( %{ $self->options } );
  3         18  
503              
504 3         9 my $base;
505 3 50       15 if ( $self->site->base_url ) {
506 3         25 $base = Mojo::URL->new( $self->site->base_url )->path->to_string;
507 3         806 $base =~ s{/$}{};
508             }
509              
510 3         11 my $index = "/index.html";
511 3 50       12 if ( $base ) {
512 0         0 $index = $base . $index;
513             }
514              
515             # Add the build dir to the list of static paths for mojolicious to
516             # search
517 3         6 unshift @{ $self->static->paths }, $self->site->build_store->path;
  3         26  
518              
519             # Watch for filesystem events and rebuild the site Right now this only
520             # works on OSX. We should spin this off into Mojo::IOLoop::FSEvents and
521             # make it work cross-platform, including a pure-Perl fallback
522 3         55 my $can_watch = eval { require Mac::FSEvents; 1 };
  3         285  
  0         0  
523 3 50 33     40 if ( !$can_watch && $^O =~ /darwin/ ) {
524 0         0 say "To watch for filesystem changes and automatically rebuild the site, ",
525             "install the Mac::FSEvents module from CPAN";
526             }
527              
528 3 50       9 if ( $can_watch ) {
529              
530             # Collect the paths to watch
531 0         0 my %watches = ();
532 0         0 for my $app ( values %{ $self->site->apps } ) {
  0         0  
533 0 0       0 if ( $app->can( 'store' ) ) {
534 0         0 push @{ $watches{ $app->store->path } }, $app->store;
  0         0  
535             }
536             }
537              
538             # Watch the theme, but not built-in themes
539 0         0 my $theme_path = $self->site->theme->store->path;
540 0 0       0 if ( !Path::Tiny->new( dist_dir( 'Statocles' ) )->subsumes( $theme_path ) ) {
541 0         0 push @{ $watches{ $theme_path } }, $self->site->theme;
  0         0  
542             }
543              
544 0         0 require Mojo::IOLoop::Stream;
545 0         0 my $ioloop = Mojo::IOLoop->singleton;
546 0         0 my $build_dir = $self->site->build_store->path->realpath;
547              
548 0         0 weaken $self;
549              
550 0         0 for my $path ( keys %watches ) {
551 0         0 $self->log->info( "Watching for changes in '$path'" );
552              
553 0         0 my $fs = Mac::FSEvents->new( {
554             path => "$path",
555             latency => 1.0,
556             } );
557 0         0 my $handle = $fs->watch;
558              
559 0         0 push @{ $self->cleanup }, sub {
560 0 0 0 0   0 return if !$fs || !$handle;
561 0         0 $fs->stop;
562 0         0 Mojo::IOLoop->remove( $handle );
563 0         0 };
564              
565             $ioloop->reactor->io( $handle, sub {
566 0     0   0 my ( $reactor, $writable ) = @_;
567              
568 0         0 my $rebuild;
569             REBUILD:
570 0         0 for my $event ( $fs->read_events ) {
571 0 0       0 if ( $event->path =~ /^\Q$build_dir/ ) {
572 0         0 next;
573             }
574              
575 0         0 $self->log->info( "Path '" . $event->path . "' changed... Rebuilding" );
576 0         0 $_->clear for @{ $watches{ $path } };
  0         0  
577 0         0 $rebuild = 1;
578             }
579              
580 0 0       0 if ( $rebuild ) {
581 0         0 $self->site->build( %{ $self->options } );
  0         0  
582             }
583 0         0 } );
584 0         0 $ioloop->reactor->watch( $handle, 1, 0 );
585             }
586              
587 0         0 $self->log->info( "Ignoring changes in '$build_dir'" );
588             }
589              
590             my $serve_static = sub {
591 0     0   0 my ( $c ) = @_;
592 0         0 my $path = Mojo::Path->new( $c->stash->{path} );
593              
594             # Taint check the path, just in case someone uses this "dev" tool to
595             # serve real content
596 0 0       0 return $c->render( status => 400, text => "You didn't say the magic word" )
597             if $path->canonicalize->parts->[0] eq '..';
598              
599 0         0 my $asset = $c->app->static->file( $path );
600 0 0       0 if ( !$asset ) {
601 0 0       0 if ( $path =~ m{/$} ) {
    0          
602             # Check for index.html
603 0         0 $path = Mojo::Path->new( $c->stash->{path} . "/index.html" );
604 0         0 $asset = $c->app->static->file( $path );
605             }
606             elsif ( $c->app->site->build_store->path->child( $path )->is_dir ) {
607 0         0 return $c->redirect_to( "/$path/" );
608             }
609             }
610              
611 0 0       0 if ( !$asset ) {
612 0         0 return $c->render( status => 404, text => 'Not found' );
613             }
614              
615             # The static helper will choose the right content type and charset
616 0         0 return $c->reply->static( $path );
617 3         21 };
618              
619 3 50       9 if ( $base ) {
620             $self->routes->get( '/', sub {
621 0     0   0 my ( $c ) = @_;
622 0         0 $c->redirect_to( $base );
623 0         0 } );
624 0         0 $self->routes->get( $base . '/*path' )->to( path => 'index.html', cb => $serve_static );
625             }
626             else {
627 3         18 $self->routes->get( '/*path' )->to( path => 'index.html', cb => $serve_static );
628             }
629              
630             }
631              
632             }
633              
634             1;
635              
636             __END__
637              
638             =pod
639              
640             =encoding UTF-8
641              
642             =head1 NAME
643              
644             Statocles::Command - The statocles command-line interface
645              
646             =head1 VERSION
647              
648             version 0.084
649              
650             =head1 SYNOPSIS
651              
652             use Statocles::Command;
653             exit Statocles::Command->main( @ARGV );
654              
655             =head1 DESCRIPTION
656              
657             This module implements the Statocles command-line interface.
658              
659             =head1 ATTRIBUTES
660              
661             =head2 site
662              
663             The L<site|Statocles::Site> we're working with.
664              
665             =head2 log
666              
667             A L<Mojo::Log> object for logging. Defaults to the current site's C<log> attribute.
668              
669             =head1 METHODS
670              
671             =head2 main
672              
673             my $exitval = $cmd->main( @argv );
674              
675             Run the command given in @argv. See L<statocles> for a list of commands and
676             options.
677              
678             =head1 SEE ALSO
679              
680             =over 4
681              
682             =item L<statocles>
683              
684             The documentation for the command-line application.
685              
686             =back
687              
688             =head1 AUTHOR
689              
690             Doug Bell <preaction@cpan.org>
691              
692             =head1 COPYRIGHT AND LICENSE
693              
694             This software is copyright (c) 2016 by Doug Bell.
695              
696             This is free software; you can redistribute it and/or modify it under
697             the same terms as the Perl 5 programming language system itself.
698              
699             =cut