File Coverage

blib/lib/ORLite/Mirror.pm
Criterion Covered Total %
statement 126 137 91.9
branch 59 84 70.2
condition 20 43 46.5
subroutine 16 16 100.0
pod n/a
total 221 280 78.9


line stmt bran cond sub pod time code
1             package ORLite::Mirror;
2              
3 8     8   690230 use 5.006;
  8         31  
  8         351  
4 8     8   44 use strict;
  8         14  
  8         201  
5 8     8   46 use Carp ();
  8         14  
  8         113  
6 8     8   7863 use File::Copy ();
  8         53430  
  8         374  
7 8     8   60 use File::Spec 0.80 ();
  8         199  
  8         211  
8 8     8   45 use File::Path 2.04 ();
  8         172  
  8         151  
9 8     8   918 use File::Remove 1.42 ();
  8         2027  
  8         159  
10 8     8   9633 use File::HomeDir 0.69 ();
  8         62959  
  8         235  
11 8     8   12721 use File::ShareDir 1.00 ();
  8         78544  
  8         257  
12 8     8   8583 use Params::Util 0.33 ();
  8         23573  
  8         229  
13 8     8   10716 use LWP::UserAgent 5.806 ();
  8         399506  
  8         261  
14 8     8   8219 use LWP::Online 1.07 ();
  8         222986  
  8         252  
15 8     8   9987 use ORLite 1.37 ();
  8         382942  
  8         374  
16              
17 8     8   101 use vars qw{$VERSION @ISA};
  8         17  
  8         731  
18             BEGIN {
19 8     8   23 $VERSION = '1.24';
20 8         11988 @ISA = 'ORLite';
21             }
22              
23              
24              
25              
26              
27             #####################################################################
28             # Code Generation
29              
30             sub import {
31 9   33 9   5602290 my $class = ref $_[0] || $_[0];
32              
33             # Check for debug mode
34 9         96 my $DEBUG = 0;
35 9 50 33     330 if ( defined Params::Util::_STRING($_[-1]) and $_[-1] eq '-DEBUG' ) {
36 0         0 $DEBUG = 1;
37 0         0 pop @_;
38             }
39              
40             # Check params and apply defaults
41 9         52 my %params;
42 9 50       268 if ( defined Params::Util::_STRING($_[1]) ) {
    50          
43             # Support the short form "use ORLite 'http://.../db.sqlite'"
44 0         0 %params = (
45             url => $_[1],
46             readonly => undef, # Automatic
47             package => undef, # Automatic
48             );
49             } elsif ( Params::Util::_HASH($_[1]) ) {
50 9         39 %params = %{ $_[1] };
  9         133  
51             } else {
52 0         0 Carp::croak("Missing, empty or invalid params HASH");
53             }
54              
55             # Check for incompatible create option
56 9 50 33     111 if ( $params{create} and ref($params{create}) ) {
57 0         0 Carp::croak("Cannot supply complex 'create' param to ORLite::Mirror");
58             }
59              
60             # Autodiscover the package if needed
61 9 50       65 unless ( defined $params{package} ) {
62 9         95 $params{package} = scalar caller;
63             }
64 9   50     306 my $pversion = $params{package}->VERSION || 0;
65 9         69 my $agent = "$params{package}/$pversion";
66              
67             # Normalise boolean settings
68 9 50       69 my $show_progress = $params{show_progress} ? 1 : 0;
69 9 50       509 my $env_proxy = $params{env_proxy} ? 1 : 0;
70              
71             # Use array-based objects by default, they are smaller and faster
72 9 100       65 unless ( defined $params{array} ) {
73 3         15 $params{array} = 1;
74             }
75              
76             # Find the maximum age for the local database copy
77 9         37 my $maxage = delete $params{maxage};
78 9 100       55 unless ( defined $maxage ) {
79 6         15 $maxage = 86400;
80             }
81 9 50       494 unless ( Params::Util::_NONNEGINT($maxage) ) {
82 0         0 Carp::croak("Invalid maxage param '$maxage'");
83             }
84              
85             # Find the stub database
86 9         242 my $stub = delete $params{stub};
87 9 100       39 if ( $stub ) {
88 4 100       41 $stub = File::ShareDir::module_file(
89             $params{package} => 'stub.db'
90             ) if $stub eq '1';
91 4 50       862 unless ( -f $stub ) {
92 0         0 Carp::croak("Stub database '$stub' does not exist");
93             }
94             }
95              
96             # Check when we should update
97 9         38 my $update = delete $params{update};
98 9 100       53 unless ( defined $update ) {
99 7 100       62 $update = $stub ? 'connect' : 'compile';
100             }
101 9 50       136 unless ( $update =~ /^(?:compile|connect)$/ ) {
102 0         0 Carp::croak("Invalid update param '$update'");
103             }
104              
105             # Determine the mirror database directory
106 9 50       310 my $dir = File::Spec->catdir(
107             File::HomeDir->my_data,
108             ($^O eq 'MSWin32' ? 'Perl' : '.perl'),
109             'ORLite-Mirror',
110             );
111              
112             # Create it if needed
113 9 100       1249 unless ( -e $dir ) {
114 7         3341 my @dirs = File::Path::mkpath( $dir, { verbose => 0 } );
115 7 50       261 $class->prune(@dirs) if $params{prune};
116             }
117              
118             # Determine the mirror database file
119 9         1195 my $file = $params{package} . '.sqlite';
120 9         76 $file =~ s/::/-/g;
121 9         142 my $db = File::Spec->catfile( $dir, $file );
122              
123             # Download compressed files with their extention first
124 9         39 my $url = delete $params{url};
125 9 100       119 my $path = ($url =~ /(\.gz|\.bz2)$/) ? "$db$1" : $db;
126 9 100       283 unless ( -f $path ) {
127 8 50       82 $class->prune($path) if $params{prune};
128             }
129              
130             # Are we online (fake to true if the URL is local)
131 9   33     204 my $online = !! ( $url =~ /^file:/ or LWP::Online::online() );
132 9 0 33     96 unless ( $online or -f $path or $stub ) {
      33        
133             # Don't have the file and can't get it
134 0         0 Carp::croak("Cannot fetch database without an internet connection");
135             }
136              
137             # If the file doesn't exist, sync at compile time.
138 9         23 my $STUBBED = 0;
139 9 100       211 unless ( -f $db ) {
140 8 100 66     73 if ( $update eq 'connect' and $stub ) {
141             # Fallback option, use the stub
142 3 50       48 File::Copy::copy( $stub => $db ) or
143             Carp::croak("Failed to copy in stub database");
144 3         1461 $STUBBED = 1;
145             } else {
146 5         40 $update = 'compile';
147             }
148 8 50       88 $class->prune($db) if $params{prune};
149             }
150              
151             # We've finished with all the pruning we'll need to do
152 9         142 $params{prune} = 0;
153              
154             # Don't update if the file is newer than the maxage
155 9   100     517 my $mtime = (stat($path))[9] || 0;
156 9         69 my $old = (time - $mtime) > $maxage;
157 9 100 33     167 if ( not $STUBBED and -f $path ? ($old and $online) : 1 ) {
    100 66        
158             # Create the default useragent
159 5         19 my $useragent = delete $params{useragent};
160 5 50       28 unless ( $useragent ) {
161 5         195 $useragent = LWP::UserAgent->new(
162             agent => $agent,
163             timeout => 30,
164             show_progress => $show_progress,
165             env_proxy => $env_proxy,
166             );
167             }
168              
169             # Fetch the archive
170 5         3489 my $response = $useragent->mirror( $url => $path );
171 5 50 33     160678 unless ( $response->is_success or $response->code == 304 ) {
172 0         0 Carp::croak("Error: Failed to fetch $url");
173             }
174              
175             # Decompress if we pulled an archive
176 5         58 my $refreshed = 0;
177 5 100       44 if ( $path =~ /\.gz$/ ) {
    100          
178 2 50 33     8 unless ( $response->code == 304 and -f $path ) {
179 2         2250 require IO::Uncompress::Gunzip;
180 2 50       38905 IO::Uncompress::Gunzip::gunzip(
181             $path => $db,
182             BinModeOut => 1,
183             ) or Carp::croak("gunzip($path) failed");
184 2         6755 $refreshed = 1;
185             }
186             } elsif ( $path =~ /\.bz2$/ ) {
187 1 50 33     4 unless ( $response->code == 304 and -f $path ) {
188 1         3737 require IO::Uncompress::Bunzip2;
189 1 50       14913 IO::Uncompress::Bunzip2::bunzip2(
190             $path => $db,
191             BinModeOut => 1,
192             ) or Carp::croak("bunzip2($path) failed");
193 1         2442 $refreshed = 1;
194             }
195             }
196              
197             # If we updated the file, add any extra indexes that we need
198 5 100 100     355 if ( $refreshed and $params{index} ) {
199 1         33 my $dbh = DBI->connect( "DBI:SQLite:$db", undef, undef, {
200             RaiseError => 1,
201             PrintError => 1,
202             } );
203 1         654 foreach ( @{$params{index}} ) {
  1         5  
204 1         7 my ($table, $column) = split /\./, $_;
205 1         19 $dbh->do("CREATE INDEX idx__${table}__${column} ON $table ( $column )");
206             }
207 1         32032 $dbh->disconnect;
208             }
209             }
210              
211             # Mirrored databases are always readonly.
212 9         41 $params{file} = $db;
213 9         163 $params{readonly} = 1;
214              
215             # If and only if they update at connect-time, replace the
216             # original dbh method with one that syncs the database.
217 9 100       108 if ( $update eq 'connect' ) {
218             # Generate the archive decompression fragment
219 4         15 my $decompress = '';
220 4 100       30 if ( $path =~ /\.gz$/ ) {
    50          
221 2         7 $decompress = <<"END_PERL";
222             unless ( \$response->code == 304 and -f \$PATH ) {
223             my \$sqlite = \$class->sqlite;
224             require File::Remove;
225             unless ( File::Remove::remove(\$sqlite) ) {
226             Carp::croak("Error: Failed to flush '\$sqlite'");
227             }
228              
229             require IO::Uncompress::Gunzip;
230             IO::Uncompress::Gunzip::gunzip(
231             \$PATH => \$sqlite,
232             BinModeOut => 1,
233             ) or Carp::croak("Error: gunzip(\$PATH) failed");
234             }
235              
236             END_PERL
237             } elsif ( $path =~ /\.bz2$/ ) {
238 0         0 $decompress = <<"END_PERL";
239             unless ( \$response->code == 304 and -f \$PATH ) {
240             my \$sqlite = \$class->sqlite;
241             require File::Remove;
242             unless ( File::Remove::remove(\$sqlite) ) {
243             Carp::croak("Error: Failed to flush '\$sqlite'");
244             }
245              
246             require IO::Uncompress::Bunzip2;
247             IO::Uncompress::Bunzip2::bunzip2(
248             \$PATH => \$sqlite,
249             BinModeOut => 1,
250             ) or Carp::croak("Error: bunzip2(\$PATH) failed");
251             }
252              
253             END_PERL
254             }
255              
256             # Combine to get the final merged append code
257 4         60 $params{append} = <<"END_PERL";
258             use Carp ();
259              
260             use vars qw{ \$REFRESHED };
261             BEGIN {
262             \$REFRESHED = 0;
263             # delete \$$params{package}::{DBH};
264             }
265              
266             my \$URL = '$url';
267             my \$PATH = '$path';
268              
269             sub refresh {
270             my \$class = shift;
271             my \%param = \@_;
272              
273             require LWP::UserAgent;
274             my \$useragent = LWP::UserAgent->new(
275             agent => '$agent',
276             timeout => 30,
277             show_progress => !! \$param{show_progress},
278             );
279              
280             # Set the refresh flag now, so the call to ->pragma won't
281             # head off into an infinite recursion.
282             \$REFRESHED = 1;
283              
284             # Save the old schema version
285             my \$old_version = \$class->pragma('user_version');
286              
287             # Flush the existing database
288             require File::Remove;
289             if ( -f \$PATH and not File::Remove::remove(\$PATH) ) {
290             Carp::croak("Error: Failed to flush '\$PATH'");
291             }
292              
293             # Fetch the archive
294             my \$response = \$useragent->mirror( \$URL => \$PATH );
295             unless ( \$response->is_success or \$response->code == 304 ) {
296             Carp::croak("Error: Failed to fetch '\$URL'");
297             }
298              
299             $decompress
300             # The new schema version must match the previous or stub version
301             my \$version = \$class->pragma('user_version');
302             unless ( \$version == \$old_version ) {
303             Carp::croak("Schema user_version mismatch (got \$version, wanted \$old_version)");
304             }
305              
306             return 1;
307             }
308              
309             no warnings 'redefine';
310             sub connect {
311             my \$class = shift;
312             unless ( \$REFRESHED ) {
313             \$class->refresh(
314             show_progress => $show_progress,
315             env_proxy => $env_proxy,
316             );
317             }
318             DBI->connect( \$class->dsn, undef, undef, {
319             RaiseError => 1,
320             PrintError => 0,
321             } );
322             }
323             END_PERL
324             }
325              
326             # Hand off to the main ORLite class
327             $class->SUPER::import(
328 9 50       182 \%params,
329             $DEBUG ? '-DEBUG' : ()
330             );
331             }
332              
333             1;
334              
335             =pod
336              
337             =head1 NAME
338              
339             ORLite::Mirror - Extend ORLite to support remote SQLite databases
340              
341             =head1 SYNOPSIS
342              
343             # Regular ORLite on a readonly SQLite database
344             use ORLite 'path/mydb.sqlite';
345            
346             # The equivalent for a remote (optionally compressed) SQLite database
347             use ORLite::Mirror 'http://myserver/path/mydb.sqlite.gz';
348            
349             # All available additional options specified
350             use ORLite::Mirror {
351             url => 'http://myserver/path/mydb.sqlite.gz',
352             maxage => 3600,
353             show_progress => 1,
354             env_proxy => 1,
355             prune => 1,
356             index => [
357             'table1.column1',
358             'table1.column2',
359             ],
360             };
361              
362             =head1 DESCRIPTION
363              
364             L provides a readonly ORM API when it loads a readonly SQLite
365             database from your local system.
366              
367             By combining this capability with L, L goes one step
368             better and allows you to load a SQLite database from any arbitrary URI in
369             readonly form as well.
370              
371             As demonstrated in the synopsis above, you using L in the
372             same way, but provide a URL instead of a file name.
373              
374             If the URL explicitly ends with a '.gz' or '.bz2' then L
375             will decompress the file before loading it.
376              
377             =head1 OPTIONS
378              
379             B adds an extensive set of options to those provided by the
380             underlying L library.
381              
382             =head2 url
383              
384             The compulsory C parameter should be a string containing the remote
385             location of the SQLite database we will be mirroring.
386              
387             B supports downloading the database compressed, and then
388             transparently decompressing the file locally. Compression support is
389             controlled by the extension on the remote database.
390              
391             The extensions C<.gz> (for gunzip) and C<.bz2> (for bunzip2) are currently
392             supported.
393              
394             =head2 maxage
395              
396             The optional C parameter controls how often B
397             should check the remote server to see if the data has been updated.
398              
399             This allows programs using the database to start quickly the majority of
400             the time, but continue to receive automatic updates periodically.
401              
402             The value is the number of integer seconds we should avoid checking the
403             remote server for. The default is 86400 seconds (one 24 hour day).
404              
405             =head2 show_progress
406              
407             The optional C parameter will be passed through to the
408             underlying L that will fetch the remote database file.
409              
410             When set to true, it causes a progress bar to be displayed on the terminal
411             as the database file is downloaded.
412              
413             =head2 env_proxy
414              
415             The optional C parameter will be passed through to the
416             underlying L that will fetch the remote database file.
417              
418             When set to true, it causes L to read the location of a
419             proxy server from the environment.
420              
421             =head2 prune
422              
423             The optional C parameter should be used when the surrounding
424             program wants to avoid leaving files on the host system.
425              
426             It causes any files or directories created during the operation of
427             B to be deleted on program exit at C-time.
428              
429             =head2 index
430              
431             One challenge when distributing SQLite database is the quantity of data
432             store on disk to support the indexes on your database.
433              
434             For a moderately indexed database where all primary and foreign key columns
435             have indexes, the amount of data in the indexes can be nearly as large as
436             the data stored for the tables themselves.
437              
438             Because each user of the database module will be interested in different
439             things, the indexes that the original creator chooses to place on the
440             database may not even be used at all and other valuable indexes may not
441             exist at all.
442              
443             To allow sufficiently flexibility, we recommend that SQLite database be
444             distributed without any indexes. This greatly reduces the file size and
445             download time for the database file.
446              
447             The optional C parameter should then be used by each different
448             consumer of that module to index just the columns that are of specific
449             interest and will be used in the queries that will be run on the database.
450              
451             The value should be set to an C reference containing a list of
452             column names in C form.
453              
454             index => [
455             'table1.column1',
456             'table1.column2',
457             ],
458              
459             =head1 SUPPORT
460              
461             Bugs should be reported via the CPAN bug tracker at
462              
463             L
464              
465             For other issues, contact the author.
466              
467             =head1 AUTHOR
468              
469             Adam Kennedy Eadamk@cpan.orgE
470              
471             =head1 COPYRIGHT
472              
473             Copyright 2008 - 2012 Adam Kennedy.
474              
475             This program is free software; you can redistribute
476             it and/or modify it under the same terms as Perl itself.
477              
478             The full text of the license can be found in the
479             LICENSE file included with this module.
480              
481             =cut