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.086';
3             # ABSTRACT: The statocles command-line interface
4              
5 7     7   158736 use Statocles::Base 'Class';
  7         13  
  7         54  
6 7     7   47345 use Scalar::Util qw( blessed );
  7         17  
  7         402  
7 7     7   38 use Getopt::Long qw( GetOptionsFromArray :config pass_through bundling no_auto_abbrev );
  7         13  
  7         67  
8 7     7   3926 use Pod::Usage::Return qw( pod2usage );
  7         2401  
  7         354  
9 7     7   1467 use File::Share qw( dist_dir );
  7         3141  
  7         313  
10 7     7   2595 use Beam::Wire;
  7         2012597  
  7         246  
11 7     7   2279 use Statocles::Template;
  7         25  
  7         17689  
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 1239534 my ( $class, @argv ) = @_;
51              
52 51         345 my %opt = (
53             config => 'site.yml',
54             site => 'site',
55             verbose => 0,
56             );
57 51         384 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       27371 return pod2usage(0) if $opt{help};
66              
67 49 100 100     384 if ( $opt{version} || ( $opt{verbose} && !@argv ) ) {
      100        
68 2         57 say "Statocles version $Statocles::Command::VERSION (Perl $^V)";
69 2         13 return 0;
70             }
71              
72 47         143 my $method = shift @argv;
73 47 100       146 return pod2usage("ERROR: Missing command") unless $method;
74              
75             # Create site does not require a config file
76 46 100       173 if ( $method eq 'create' ) {
77             # Create a custom logger
78 7         81 my $log = Mojo::Log->new;
79 7         292 $log->handle( \*STDOUT );
80 7         77 $log->level( $VERBOSE[ $opt{verbose} ] );
81 7         193 return $class->new( log => $log )->create_site( \@argv, \%opt );
82             }
83              
84 39 100       577 if ( !-e $opt{config} ) {
85 2         67 warn sprintf qq{ERROR: Could not find config file "\%s"\n}, $opt{config};
86 2         12 return 1;
87             }
88              
89 37         90 my $wire = eval { Beam::Wire->new( file => $opt{config} ) };
  37         968  
90              
91 37 100       988686 if ( $@ ) {
92 4 50 33     465 if ( blessed $@ && $@->isa( 'Beam::Wire::Exception::Config' ) ) {
93 4         10 my $remedy;
94 4 100 66     12 if ( $@ =~ /found character that cannot start any token/ || $@ =~ /YAML_PARSE_ERR_NONSPACE_INDENTATION/ ) {
    50 33        
    0 0        
95 2         85 $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         177 $remedy = "Check your indentation. ";
99             }
100             elsif ( $@ =~ /Syck parser/ && $@ =~ /syntax error/ ) {
101 0         0 $remedy = "Check your indentation. ";
102             }
103              
104 4 100       21 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       160 ( $opt{verbose} ? "\n\nRaw error: $@" : "" )
112             ;
113              
114 4         192 return 1;
115             }
116 0         0 die $@;
117             }
118              
119 33         91 my $site = eval { $wire->get( $opt{site} ) };
  33         221  
120              
121 33 100       51411 if ( $@ ) {
122 4 100 66     369 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         118 $opt{site}, $opt{config};
125 2         40 return 1;
126             }
127             warn sprintf qq{ERROR: Could not create site object "%s" in config file "%s": %s\n},
128 2         80 $opt{site}, $opt{config}, $@;
129 2         94 return 1;
130             }
131              
132 29         567 my $cmd = $class->new( site => $site );
133              
134 29 100       7987 if ( $opt{verbose} ) {
135 4         91 $cmd->site->log->handle( \*STDOUT );
136 4         467 $cmd->site->log->level( $VERBOSE[ $opt{verbose} ] );
137             }
138              
139 29 100       270 if ( $method eq 'build' ) {
    100          
    100          
    50          
    100          
    100          
140 11         20 my %build_opt;
141 11         77 GetOptionsFromArray( \@argv, \%build_opt,
142             'date|d=s',
143             );
144 11         2070 $cmd->site->build( %build_opt );
145 11         511 return 0;
146             }
147             elsif ( $method eq 'deploy' ) {
148 7         17 my %deploy_opt;
149 7         49 GetOptionsFromArray( \@argv, \%deploy_opt,
150             'date|d=s',
151             'clean',
152             'message|m=s',
153             );
154 7         2403 $cmd->site->deploy( %deploy_opt );
155              
156 7         12622 return 0;
157             }
158             elsif ( $method eq 'apps' ) {
159 1         6 my $apps = $cmd->site->apps;
160 1         3 for my $app_name ( keys %{ $apps } ) {
  1         5  
161 2         6 my $app = $apps->{$app_name};
162 2         9 my $root = $app->url_root;
163 2         5 my $class = ref $app;
164 2         77 say "$app_name ($root -- $class)";
165             }
166 1         73 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         7 my %build_opt;
186 3         27 GetOptionsFromArray( \@argv, \%build_opt,
187             'date|d=s',
188             );
189              
190 3         709 require Mojo::Server::Daemon;
191 3         55 my $app = Statocles::Command::_MOJOAPP->new(
192             site => $cmd->site,
193             options => \%build_opt,
194             );
195 3         973 our $daemon = Mojo::Server::Daemon->new(
196             silent => 1,
197             app => $app,
198             );
199              
200 3 100       806 if ( $opt{port} ) {
201 1         8 $daemon->listen([ "http://*:$opt{port}" ]);
202             }
203              
204             # Using start() instead of run() so we can stop() inside the tests
205 3         24 $daemon->start;
206              
207             # Find the port we're listening on
208 3         4474 my $id = $daemon->acceptors->[0];
209 3         17 my $handle = $daemon->ioloop->acceptor( $id )->handle;
210 3   50     46 say "Listening on " . sprintf( 'http://%s:%d', $handle->sockhost || '127.0.0.1', $handle->sockport );
211              
212             # Give control to the IOLoop
213 3         287 Mojo::IOLoop->start;
214             }
215             elsif ( $method eq 'bundle' ) {
216 4         11 my $what = $argv[0];
217 4 50       18 if ( $what eq 'theme' ) {
218 4         9 my $theme_name = $argv[1];
219 4 100       12 if ( !$theme_name ) {
220 1         85 say STDERR "ERROR: No theme name!";
221 1         7 say STDERR "\nUsage:\n\tstatocles bundle theme <name>";
222 1         21 return 1;
223             }
224              
225 3         27 my $dest_dir = $site->theme->store->path;
226 3         22 $cmd->bundle_theme( $theme_name, $dest_dir, @argv[2..$#argv] );
227 3         885 say qq{Theme "$theme_name" written to "$dest_dir"};
228             }
229             }
230             else {
231 3         10 my $app_name = $method;
232 3         19 my $app = $cmd->site->apps->{ $app_name };
233              
234 3 100       29 if ( !$app ) {
    100          
235 1         7 return pod2usage("ERROR: Unknown command or app '$app_name'");
236             }
237             elsif ( !$app->can( 'command' ) ) {
238 1         64 say STDERR sprintf 'ERROR: Application "%s" has no commands', $app_name;
239 1         36 return 1;
240             }
241              
242 1         11 return $cmd->site->apps->{ $app_name }->command( $app_name, @argv );
243             }
244              
245 6         1215 return 0;
246             }
247              
248             sub create_site {
249 7     7 0 1762 my ( $self, $argv, $opt ) = @_;
250              
251 7         15 my %answer;
252 7         19 my $site_root = '.';
253              
254             # Allow the user to set the base URL and the site folder as an argument
255 7 100       27 if ( @$argv ) {
256 5         12 my $base = $argv->[0];
257 5 100       32 if ( $base =~ m{^https?://(.+)} ) {
258 3         11 $answer{base_url} = $base;
259 3   66     18 $site_root = $argv->[1] || $1;
260             }
261             else {
262 2         10 $answer{base_url} = "http://$base";
263 2   33     12 $site_root = $argv->[1] || $base;
264             }
265             }
266              
267 7         40 my $create_dir = Path::Tiny->new( dist_dir( 'Statocles' ) )->child( 'create' );
268 7         1479 my $question = YAML::Load( $create_dir->child( 'script.yml' )->slurp_utf8 );
269 7         94402 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         449 print "\n", $question->{flavor};
279 7         56 print "\n", "\n", $prompt{flavor}, " ";
280 7         203 chomp( $answer{flavor} = <STDIN> );
281 7         123 until ( $answer{flavor} =~ /^[120]*$/ ) {
282 0         0 print $prompt{flavor}, " ";
283 0         0 chomp( $answer{flavor} = <STDIN> );
284             }
285 7 50       32 $answer{flavor} = 1 if $answer{flavor} eq '';
286              
287 7         68 print "\n", "\n", $question->{bundle_theme};
288 7         41 print "\n", "\n", $prompt{bundle_theme}, " ";
289 7         31 chomp( $answer{bundle_theme} = <STDIN> );
290 7         44 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       38 $answer{bundle_theme} = "y" if $answer{bundle_theme} eq '';
295              
296 7 100       27 if ( !$answer{base_url} ) {
297 2         14 print "\n", "\n", $question->{base_url};
298 2         9 print "\n", "\n", $prompt{base_url}, " ";
299 2         7 chomp( $answer{base_url} = <STDIN> );
300 2 100       13 if ( $answer{base_url} !~ m{^https?://} ) {
301 1         4 $answer{base_url} = "http://$answer{base_url}";
302             }
303             }
304              
305 7         44 print "\n", "\n", $question->{deploy_class};
306 7         35 print "\n", "\n", $prompt{deploy_class}, " ";
307 7         27 chomp( $answer{deploy_class} = <STDIN> );
308 7         41 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       29 $answer{deploy_class} = 1 if $answer{deploy_class} eq '';
313              
314 7 100       38 if ( $answer{deploy_class} == 1 ) {
    100          
315             # Git deploy questions
316 2         17 print "\n", "\n", $question->{git_branch};
317 2         14 print "\n", "\n", $prompt{git_branch}, " ";
318 2         10 chomp( $answer{git_branch} = <STDIN> );
319 2   50     19 $answer{git_branch} ||= "master";
320             }
321             elsif ( $answer{deploy_class} == 2 ) {
322             # File deploy questions
323 3         20 print "\n", "\n", $question->{deploy_path};
324 3         15 print "\n", "\n", $prompt{deploy_path}, " ";
325 3         11 chomp( $answer{deploy_path} = <STDIN> );
326 3   50     9 $answer{deploy_path} ||= '.';
327             }
328              
329             ### Build the site
330 7         34 my $cwd = cwd;
331 7         326 my $root = Path::Tiny->new( $site_root );
332 7         187 $root->mkpath;
333 7         971 my $config_tmpl = Statocles::Template->new(
334             path => $create_dir->child( 'site.yml' ),
335             );
336 7         335 my %vars;
337              
338 7 100       41 if ( $answer{flavor} == 1 ) {
    100          
339 2         11 $vars{site}{index} = "/blog";
340 2         17 $vars{site}{nav}{main}[0] = {
341             href => "/",
342             text => "Blog",
343             };
344             }
345             elsif ( $answer{flavor} == 2 ) {
346 3         10 $vars{site}{index} = "/page";
347 3         17 $vars{site}{nav}{main}[0] = {
348             href => "/blog",
349             text => "Blog",
350             };
351             }
352             else {
353 2         9 $vars{site}{index} = "/blog";
354 2         12 $vars{site}{nav}{main}[0] = {
355             href => "/",
356             text => "Blog",
357             };
358             }
359              
360 7 100       37 if ( lc $answer{bundle_theme} eq 'y' ) {
361 2         10 chdir $root;
362 2         28 $self->bundle_theme( 'default', 'theme' );
363 2         700 chdir $cwd;
364 2         42 $vars{theme}{args}{store} = 'theme';
365             }
366             else {
367 5         22 $vars{theme}{args}{store} = '::default';
368             }
369              
370 7 50       33 if ( $answer{base_url} ) {
371 7         20 $vars{site}{base_url} = $answer{base_url};
372             }
373              
374 7 100       33 if ( $answer{deploy_class} == 1 ) {
    100          
375 2         13 $vars{deploy}{class} = 'Statocles::Deploy::Git';
376 2         10 $vars{deploy}{args}{branch} = $answer{git_branch};
377              
378             # Create the git repo
379 2         19 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         8 chdir $root;
383 2         61 Git::Repository->run( 'init' );
384 2         54597 chdir $cwd;
385 2         73 $root->child( '.gitignore' )->append( "\n.statocles\n" );
386             }
387             elsif ( $answer{deploy_class} == 2 ) {
388 3         10 $vars{deploy}{class} = 'Statocles::Deploy::File';
389 3         8 $vars{deploy}{args}{path} = $answer{deploy_path};
390             }
391             else {
392             # We need a deploy in order to create a Site object
393 2         7 $vars{deploy}{class} = 'Statocles::Deploy::File';
394 2         6 $vars{deploy}{args}{path} = '.';
395             }
396              
397 7         707 $root->child( 'site.yml' )->spew_utf8( $config_tmpl->render( %vars ) );
398 7         3513 my ( $site ) = YAML::Load( $root->child( 'site.yml' )->slurp_utf8 );
399              
400             # Make required store directories
401 7         144505 for my $app ( map { $_->{'$ref'} } values %{ $site->{site}{args}{apps} } ) {
  21         80  
  7         74  
402 21         3011 my $path = $site->{$app}{args}{store};
403 21 50       56 next unless $path;
404 21         71 $root->child( $path )->mkpath;
405             }
406              
407             ### Copy initial site content
408             # Blog
409 7 50       871 if ( my $ref = $site->{site}{args}{apps}{blog} ) {
410 7         27 my $path = $site->{ $ref->{ '$ref' } }{args}{store};
411 7 50       26 if ( $path ) {
412 7         264 my ( undef, undef, undef, $day, $mon, $year ) = localtime;
413 7         32 $year += 1900;
414 7         15 $mon += 1;
415              
416 7         64 my @date_parts = (
417             sprintf( '%04i', $year ),
418             sprintf( '%02i', $mon ),
419             sprintf( '%02i', $day ),
420             );
421              
422 7         27 my $post_path = $root->child( $path, @date_parts, 'first-post', 'index.markdown' );
423 7         340 $post_path->parent->mkpath;
424 7         2195 $create_dir->child( 'blog', 'post.markdown' )->copy( $post_path );
425             }
426             }
427              
428             # Page
429 7 50       3512 if ( my $ref = $site->{site}{args}{apps}{page} ) {
430 7         39 my $path = $site->{ $ref->{ '$ref' } }{args}{store};
431 7 50       24 if ( $path ) {
432 7         28 my $page_path = $root->child( $path, 'index.markdown' );
433 7         280 $page_path->parent->mkpath;
434 7         726 $create_dir->child( 'page', 'index.markdown' )->copy( $page_path );
435             };
436             }
437              
438             ### DONE!
439 7         2393 print "\n", "\n", $question->{finish}, "\n", "\n";
440              
441 7         680 return 0;
442             }
443              
444             sub bundle_theme {
445 5     5 0 22 my ( $self, $name, $dir, @files ) = @_;
446 5         39 my $theme_dest = Path::Tiny->new( $dir );
447 5         143 my $theme_root = Path::Tiny->new( dist_dir( 'Statocles' ), 'theme', $name );
448              
449 5 100       1002 if ( !@files ) {
450 4         34 my $iter = $theme_root->iterator({ recurse => 1 });
451 4         152 while ( my $path = $iter->() ) {
452 116 100       9138 next unless $path->is_file;
453 88         953 my $relative = $path->relative( $theme_root );
454 88         13517 push @files, $relative;
455             }
456             }
457             else {
458 1         3 @files = map { Path::Tiny->new( $_ ) } @files;
  4         64  
459             }
460              
461 5         158 for my $path ( @files ) {
462 92         25670 my $abs_path = $path->absolute( $theme_root );
463 92         7348 my $dest = $theme_dest->child( $path );
464             # Don't overwrite site-customized hooks
465 92 100 100     2988 next if ( $abs_path->stat->size == 0 && $dest->exists );
466 87         26735 $self->log->debug( sprintf 'Copying theme file "%s" to "%s"', $path, $dest );
467 87 100       3754 $dest->remove if $dest->exists;
468 87         2032 $dest->parent->mkpath;
469 87         10619 $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   59 use Mojo::Base 'Mojolicious';
  7         14  
  7         49  
485 7     7   1069854 use Scalar::Util qw( weaken );
  7         17  
  7         313  
486 7     7   39 use File::Share qw( dist_dir );
  7         14  
  7         7548  
487             has 'site';
488             has options => sub { {} };
489             has cleanup => sub { Mojo::Collection->new };
490              
491             sub DESTROY {
492 2     2   129 my ( $self, $in_global_destruction ) = @_;
493 2 50       10 return unless $self->cleanup;
494 2     0   20 $self->cleanup->each( sub { $_->() } );
  0         0  
495             }
496              
497             sub startup {
498 3     3   13190 my ( $self ) = @_;
499 3         15 $self->log( $self->site->log );
500              
501             # First build the site
502 3         144 $self->site->build( %{ $self->options } );
  3         18  
503              
504 3         11 my $base;
505 3 50       20 if ( $self->site->base_url ) {
506 3         38 $base = Mojo::URL->new( $self->site->base_url )->path->to_string;
507 3         676 $base =~ s{/$}{};
508             }
509              
510 3         9 my $index = "/index.html";
511 3 50       13 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         8 unshift @{ $self->static->paths }, $self->site->build_store->path;
  3         25  
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         58 my $can_watch = eval { require Mac::FSEvents; 1 };
  3         328  
  0         0  
523 3 50 33     43 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       12 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         25 };
618              
619 3 50       15 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         20 $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.086
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