#! /usr/bin/perl use strict ; use warnings ; use JSON::XS ; use CGI qw/:standard -utf8/ ; use DBI ; use Carp ; my $DB_FILE = '/var/qdb/qdb.lite' ; my $MAX_RES = 1000 ; my $REQ_BAD = '400 Bad Request' ; my $REQ_OK = '200 OK' ; my $LINK = 'src/stable.tar.bz2' ; my $prog = substr $0, rindex ( $0, '/' ) + 1 ; my $Usage = <Intro
This service provides access to CPAN's update history.

Usage

To use the service, you must issue a web-request ; see the next section for an overview.

Each request returns a json-text :

  { "cmd" : ...  # the request
  , "err" : ...  # any errors found
  , "res" : ...  # the result
  }
If \$err is defined, then

Requests

database queries

count
The result is the number of events in the history.
Example : count
first_id
The result is the id of the first event.
Example : first_id
last_id
The result is the id of the last (most recent) event.
Example : last_id
from/\$n
The result is an id-ordered list of the oldest events with id ≥ \$n ;
at most $MAX_RES events are returned.
Example : from/7000
from/\$n/limit/\$x
same as from/ but at most min(\$x,$MAX_RES) events are returned.
Example : from/7000/limit/8
last/\$x
The result is an id-ordered list of the last min(\$x,$MAX_RES) events.
Examples : last/8 ; last/32
path/\$path
The result is a list of (0 or 1) events pertaining to \$path.
Example : path/$LINK

filesystem queries

stat/\$path
The result is [stat "/path/to/cpan/\$path"]
Example : stat/$LINK
lstat/\$path
The result is [lstat "/path/to/cpan/\$path"]
Example : lstat/$LINK
readlink/\$path
The result is readlink "/path/to/cpan/\$path"
Example : readlink/$LINK
type/\$path
The result is file, link, dir, other or undef.
Example : type/$LINK

Notes

HELP } my $header_sent = 0 ; binmode ( STDOUT => ':utf8' ) ; sub TAIL { my $date = scalar localtime ; < webmaster\@cs.uu.nl - $date Valid CSS valid-xhtml10

TAIL } sub page { my $body = shift ; my $type = shift || 'plain' ; my $status = shift || '200 OK' ; print header ( -charset => 'utf-8' , -type => "text/$type" , -status => $status ) unless $header_sent ++ ; print start_html ( -title => 'qdb help' , -encoding => 'utf-8' , -style => { -code => STYLE } ) if $type eq 'html' ; print $body ; print TAIL, end_html if $type eq 'html' ; } sub as_json { my $x = shift ; JSON::XS -> new -> canonical(1) -> pretty -> encode ( ref $x ? $x : { scalar => $x } ) ; } sub db_conn { my $file = shift ; my $dbh = DBI -> connect ( "dbi:SQLite:dbname=$file", "", "" , { AutoCommit => 1 , RaiseError => 0 } ) ; $dbh ; } sub db_select { my $dbh = shift ; my %opts = ( select => '*' , from => 'events' , where => undef , order_by => undef , limit => undef , args => [] , @_ ) ; my $qwe = join ' ', grep { length $_ } map { my $kwd = $_ ; $kwd =~ s/_/ / ; my $opt = $opts { $_ } ; ( defined $opt ? "$kwd $opt" : '' ) ; } qw(select from where order_by limit) ; print "qwe [$qwe]\n" if $opt{d} ; my $sth = $dbh -> prepare ( $qwe ) or return ( "bad qwe [$qwe]", undef ) ; $sth -> execute ( @{ $opts { args } } ) or return ( "can't exec [$qwe]", undef ) ; my $res = [] ; while ( my @row = $sth -> fetchrow_array ) { if ( @row ) { push @$res, \@row ; } else { if ( my $err = $sth -> err ) { return ( "fetch failed [$err]", undef ) ; } } } ( undef, $res ) ; } sub type { my $path = shift ; return undef unless lstat $path ; return 'link' if -l _ ; return 'dir' if -d _ ; return 'file' if -f _ ; return 'other' ; } my $dbh = db_conn $DB_FILE or Error "can't connect [$DB_FILE]" ; my $INFO = $ENV { PATH_INFO } || '' ; $INFO =~ s!/+!/!g ; $INFO =~ s!^/!! ; my $RES ; my $ERR ; my $PATH ; if ( $INFO eq '' or $INFO eq 'help' ) { page HELP, 'html' ; exit ; } elsif ( $INFO eq 'env' ) { $RES = \%ENV ; } elsif ( $INFO =~ /^(count|first_id|last_id|from|path|last)/ ) { my $cols = 'id, type, path' ; if ( $INFO =~ /^count$/ ) { ( $ERR, $RES ) = db_select ( $dbh, select => "count(1)" ) ; $RES = $RES -> [ 0 ] [ 0 ] unless $ERR ; } elsif ( $INFO =~ /^first_id$/ ) { ( $ERR, $RES ) = db_select ( $dbh, select => "MIN(id)" ) ; $RES = $RES -> [ 0 ] [ 0 ] unless $ERR ; } elsif ( $INFO =~ /^last_id$/ ) { ( $ERR, $RES ) = db_select ( $dbh, select => "MAX(id)" ) ; $RES = $RES -> [ 0 ] [ 0 ] unless $ERR ; } elsif ( $INFO =~ m!^from/(\d+)$! ) { ( $ERR, $RES ) = db_select ( $dbh , select => $cols , where => "id >= $1" , order_by => 'id' , limit => $MAX_RES ) ; } elsif ( $INFO =~ m!^from/(\d+)/limit/(\d+)$! ) { my $lim = $2 ; $lim = $MAX_RES if $lim > $MAX_RES ; ( $ERR, $RES ) = db_select ( $dbh , select => $cols , where => "id >= $1" , order_by => 'id' , limit => $lim ) ; } elsif ( $INFO =~ m!^last/(\d+)$! ) { my $lim = $1 ; $lim = $MAX_RES if $lim > $MAX_RES ; ( $ERR, $RES ) = db_select ( $dbh , select => $cols , order_by => 'id desc' , limit => $lim ) ; $RES = [ reverse @$RES ] unless $ERR ; } elsif ( $INFO =~ m!^path/! ) { my $path = $' ; if ( grep $_ eq '..', split m!/!, $path ) { $ERR = "bad path [$path]" ; } else { ( $ERR, $RES ) = db_select ( $dbh , select => $cols , where => 'path = $1' , args => [ $path ] ) ; } } else { $ERR = "bad query [$INFO]" ; } } elsif ( $INFO =~ m!^(type|stat|lstat|readlink)/! ) { my $cmd = $1 ; my $path = $' ; my $PATH = '/var/ftp/mirror/CPAN/' . $path ; if ( grep $_ eq '..', split m!/!, $path ) { $ERR = "bad path [$path]" ; } elsif ( $INFO =~ m!^type/! ) { $RES = type $PATH ; } elsif ( $INFO =~ m!^stat/! ) { $RES = [ stat $PATH ] ; } elsif ( $INFO =~ m!^lstat/! ) { $RES = [ lstat $PATH ] ; } elsif ( $INFO =~ m!^readlink/! ) { $RES = readlink $PATH ; } else { $ERR = "bad query" ; } } else { $ERR = "bad command [$INFO]" ; } my $text = as_json { cmd => $INFO, err => $ERR, res => $RES } ; page $text, 'plain', ( $ERR ? $REQ_BAD : $REQ_OK ) ;