File Coverage

blib/lib/Mojolicious/Plugin/Export.pm
Criterion Covered Total %
statement 94 98 95.9
branch 45 52 86.5
condition 13 18 72.2
subroutine 8 8 100.0
pod 2 2 100.0
total 162 178 91.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Export;
2             our $VERSION = '0.008';
3             # ABSTRACT: Export a Mojolicious website to static files
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Mojolicious::Lite;
8             #pod get '/' => 'index';
9             #pod get '/secret' => 'secret';
10             #pod plugin Export => {
11             #pod pages => [qw( / /secret )],
12             #pod };
13             #pod app->start;
14             #pod
15             #pod =head1 DESCRIPTION
16             #pod
17             #pod Deploy a Mojolicious webapp to static files.
18             #pod
19             #pod =head2 Configuration
20             #pod
21             #pod Default values for the command's options can be specified in the
22             #pod configuration using one of Mojolicious's configuration plugins.
23             #pod
24             #pod # myapp.conf
25             #pod {
26             #pod export => {
27             #pod # Configure the default pages to export
28             #pod pages => [ '/', '/hidden' ],
29             #pod # The directory to export to
30             #pod to => '/var/www/html',
31             #pod # Rewrite URLs to include base directory
32             #pod base => '/',
33             #pod }
34             #pod }
35             #pod
36             #pod =head1 HELPERS
37             #pod
38             #pod =head2 export
39             #pod
40             #pod The C helper returns the L object.
41             #pod
42             #pod =head1 EVENTS
43             #pod
44             #pod =head2 before_write
45             #pod
46             #pod Emitted after all the content has been loaded and prepared for export.
47             #pod The event is given two arguments: The Mojolicious::Plugin::Export object
48             #pod and a hashref of paths mapped to content to be exported. The content can
49             #pod be either a Mojo::DOM object for HTML pages or the content to be
50             #pod exported. This event may modify the hashref or the DOM objects to change
51             #pod the content before it is written.
52             #pod
53             #pod app->export->on( before_write => sub {
54             #pod my ( $export, $pages ) = @_;
55             #pod for my $path ( keys %$pages ) {
56             #pod my $content = $pages->{ $path };
57             #pod # ...
58             #pod }
59             #pod } );
60             #pod
61             #pod =head1 SEE ALSO
62             #pod
63             #pod L, L, L
64             #pod
65             #pod =cut
66              
67 2     2   25928 use Mojo::Base 'Mojolicious::Plugin';
  2         6  
  2         16  
68 2     2   353 use Mojo::Base 'Mojo::EventEmitter';
  2         7  
  2         8  
69 2     2   294 use Mojo::File qw( path );
  2         6  
  2         116  
70 2     2   14 use Mojo::Util qw( encode decode );
  2         5  
  2         4010  
71              
72             #pod =attr pages
73             #pod
74             #pod The pages to export by default. This can be overridden by the arguments to
75             #pod L.
76             #pod
77             #pod # Add pages to export by default
78             #pod push @{ $app->export->pages }, '/blog';
79             #pod
80             #pod =cut
81              
82             has pages => sub { [] };
83              
84             #pod =attr to
85             #pod
86             #pod The path to export to by default.
87             #pod
88             #pod =cut
89              
90             has to => '.';
91              
92             #pod =attr base
93             #pod
94             #pod The base URL, if URLs need to be rewritten.
95             #pod
96             #pod =cut
97              
98             has base => '';
99              
100             #pod =attr quiet
101             #pod
102             #pod If true, will not report every action taken by the plugin. Defaults to true.
103             #pod
104             #pod =cut
105              
106             has quiet => 1;
107              
108             has _app =>;
109              
110             sub register {
111 8     8 1 671 my ( $self, $app, $plugin_conf ) = @_;
112 8         43 $self->_app( $app );
113 8     9   133 $app->helper( export => sub { $self } );
  9         4292  
114              
115             # Config file overrides plugin config
116 8 50       833 my $config = $app->can( 'config' ) ? $app->config->{export} : {};
117 8         313 for my $key ( keys %$config ) {
118 3 50       26 if ( !$self->can( $key ) ) {
119 0         0 die "Unrecognized export configuration: $key\n";
120             }
121 3   33     16 $self->$key( $config->{ $key } // $plugin_conf->{ $key } );
122             }
123              
124 8         38 return $self;
125             }
126              
127             #pod =method export
128             #pod
129             #pod app->export->export( $override );
130             #pod
131             #pod Export the site. C<$override> is a hash reference to override the object
132             #pod attributes (keys are attribute names, values are the overridden value).
133             #pod
134             #pod =cut
135              
136             sub export {
137 8     8 1 40 my ( $self, $opt ) = @_;
138 8         31 for my $key ( qw( pages to base quiet ) ) {
139 32   100     209 $opt->{ $key } //= $self->$key;
140             }
141              
142 8 50 66     50 if ( $opt->{base} && $opt->{base} =~ m{^[^/]} ) {
143 0         0 $opt->{base} = '/' . $opt->{base};
144             }
145              
146 8         40 my $root = path( $opt->{ to } );
147             my @pages
148 8 50       134 = @{ $opt->{pages} } ? map { m{^/} ? $_ : "/$_" } @{ $opt->{pages} }
  8 100       44  
  5         38  
  5         16  
149             : ( '/' );
150              
151 8         75 my $ua = Mojo::UserAgent->new;
152 8         75 $ua->server->app( $self->_app );
153              
154             # A hash of path => knowledge about the path
155             # link_from => a hash of path -> array of DOM elements linking to original path
156             # res => The response from the request for this page
157             # redirect_to => The redirect location, if it was a redirect
158 8         479 my %history;
159              
160 8         42 while ( my $page = shift @pages ) {
161 40 50       187 next if $history{ $page }{ res };
162 40         223 my $tx = $ua->get( $page );
163 40         610084 my $res = $tx->res;
164              
165             # Do not try to write error messages
166 40 100       274 if ( $res->is_error ) {
167 5 50       126 if ( !$opt->{quiet} ) {
168 5         21 say sprintf " [ERROR] %s - %s %s",
169             $page, $res->code, $res->message;
170             }
171 5         497 next;
172             }
173              
174             # Rewrite links to redirects
175 35 100       856 if ( $res->is_redirect ) {
176 5         97 my $loc = $history{ $page }{ redirect_to } = $res->headers->location;
177 5         114 for my $link_from ( keys %{ $history{ $page }{ link_from } } ) {
  5         34  
178 5         13 for my $el ( @{ $history{ $page }{ link_from }{ $link_from } } ) {
  5         20  
179 5         41 $el->attr( href => $loc );
180             }
181             }
182 5         217 next;
183             }
184              
185 30         546 my $type = $res->headers->content_type;
186 30 100 66     859 if ( $type and $type =~ m{^text/html} and my $dom = $res->dom ) {
      66        
187 25 100       17934 my $dir = $page =~ m{/$} ? path( $page ) : path( $page )->dirname;
188 25         1918 for my $attr ( qw( href src ) ) {
189 50         2424 for my $el ( $dom->find( "[$attr]" )->each ) {
190 58         13353 my $url = $el->attr( $attr );
191              
192             # Don't analyze full URLs
193 58 100       1174 next if $url =~ m{^(?:[a-zA-Z]+:|//)};
194             # Don't analyze in-page fragments
195 42 100       144 next if $url =~ m{^#};
196              
197             # Fix relative paths
198 37 100       152 my $path = $url =~ m{^/} ? $url : $dir->child( $url )."";
199             # Remove fragment
200 37         464 $path =~ s/#.+//;
201              
202 37 50       157 if ( my $loc = $history{ $path }{ redirect_to } ) {
203 0         0 $el->attr( $attr => $loc );
204 0         0 next;
205             }
206             else {
207 37         77 push @{ $history{ $path }{ link_from }{ $page } }, $el;
  37         159  
208             }
209              
210 37 100       127 if ( !$history{ $path }{ res } ) {
211 32         112 push @pages, $path;
212             }
213              
214             }
215             }
216             }
217              
218 30         4516 $history{ $page }{ res } = $res;
219             }
220              
221             # Event for checking the status of everything we're about to export.
222             # We do this before rewriting the base URLs to make it easier to
223             # check for broken links.
224             my %to_export =
225 30 100       572 map { $_->[0] => $_->[1] =~ m{^text/html} ? $_->[2]->dom : $_->[2]->body }
226 30         449 map { [ $_, $history{ $_ }{ res }->headers->content_type, $history{ $_ }{ res } ] }
227 8         55 grep { $history{ $_ }{ res } }
  40         113  
228             keys %history;
229              
230 8         225 $self->emit( before_write => \%to_export );
231              
232 8         420 for my $page ( keys %to_export ) {
233 31         4186 my $content = $to_export{ $page };
234 31 100       143 if ( ref $content eq 'Mojo::DOM' ) {
235 25         98 my $dir = path( $page )->dirname;
236 25         1659 for my $attr ( qw( href src ) ) {
237 50         1702 for my $el ( $content->find( "[$attr]" )->each ) {
238 58         11412 my $url = $el->attr( $attr );
239             # Rewrite absolute paths
240 58 100 100     1067 if ( $opt->{base} && $url =~ m{^/} ) {
241 5 100       57 my $base_url = $url eq '/' ? $opt->{base} : $opt->{base} . $url;
242 5         19 $el->attr( $attr => $base_url );
243             }
244             }
245             }
246             }
247 31         4316 $self->_write( $root, $page, $content, $opt->{quiet} );
248             }
249             }
250              
251             sub _write {
252 31     31   110 my ( $self, $root, $page, $content, $quiet ) = @_;
253 31 100       109 if ( ref $content eq 'Mojo::DOM' ) {
254             # Mojolicious automatically decodes using the response content
255             # type, so all we need to do is encode it into the file content
256             # type that we want
257             # TODO: Allow configuring the destination encoding
258             # TODO: Ensure all text/* MIME types use the destination
259             # encoding
260 25         104 $content = encode 'utf8', $content;
261             }
262 31         5943 my $to = $root->child( $page );
263 31 100       889 if ( $to !~ m{[.][^/.]+$} ) {
264 25         269 $to = $to->child( 'index.html' );
265             }
266              
267 31         526 my $dir = $to->dirname;
268 31 100       1421 if ( !-d $dir ) {
269 12         690 $dir->make_path;
270 12 100       3023 say " [mkdir] $dir" unless $quiet;
271             }
272             else {
273 19 100       652 say " [exist] $dir" unless $quiet;
274             }
275              
276 31 100       1613 say " [write] $to" unless $quiet;
277 31         542 $to->spurt( $content );
278             }
279              
280             1;
281              
282             __END__