User Tools

Site Tools


Differences

This shows you the differences between two versions of the page.

Link to this comparison view

recvi [2018-08-01 12:45] (current)
Line 1: Line 1:
 +======RecVi,​ mini-tool for PBX REcord VIsualisation and download======
  
 +RecVi - mini-tool for PBX REcord VIsualisation and download, recvi for short. =)
 +
 +It is minimal one-page application,​ written during ~1.5-2 hours.
 +
 +{{ :​screenshot-2017-10-23-08-42-01.png?​w=720 |}}
 +
 +
 +<code perl recvi.pl.in>​
 +
 +#​!/​usr/​bin/​env perl
 +
 +#​------------------
 +#--- CONTROLLER ---
 +#​------------------
 +
 +package Recvi::​Controller;​
 +
 +use utf8;
 +use strict;
 +use warnings;
 +use Mojo::Base '​Mojolicious::​Controller';​
 +use Mojo::Util qw(md5_sum dumper quote encode);
 +use Mojo::JSON qw(encode_json decode_json);​
 +use Apache::​Htpasswd;​
 +use File::​Basename qw(fileparse);​
 +
 +sub hello {
 +    my $self = shift;
 +    $self->​render(template => '​hello'​);​
 +}
 +
 +#--- controller::​agent ---
 +
 +sub renderFile {
 +        my $self = shift;
 +        my %args = @_;
 +
 +        utf8::​decode($args{filename}) if $args{filename} && !utf8::​is_utf8($args{filename});​
 +        utf8::​decode($args{filepath}) if $args{filepath} && !utf8::​is_utf8($args{filepath});​
 +
 +        my $filename = $args{filename};​
 +        my $status = $args{status} || 200;
 +        my $content_disposition = $args{content_disposition} ​ || '​attachment';​
 +        my $cleanup = $args{cleanup} // 0;
 +
 +        # Content type based on format
 +        my $content_type;​
 +        $content_type = $self->​app->​types->​type( $args{format} ) if $args{format};​
 +        $content_type = '​audio/​vnd.wave';​
 +
 +        # Create asset
 +        my $asset;
 +        if ( my $filepath = $args{filepath} ) {
 +            unless ( -f $filepath && -r $filepath ) {
 +                $self->​app->​log->​error("​Cannot read file [$filepath]. error [$!]"​);​
 +                return $self->​rendered(404);​
 +            }
 +            $filename ||= fileparse($filepath);​
 +            $asset = Mojo::​Asset::​File->​new( path => $filepath );
 +            $asset->​cleanup($cleanup);​
 +        } elsif ( $args{data} ) {
 +            $filename ||= $self->​req->​url->​path->​parts->​[-1] || '​download';​
 +            $asset = Mojo::​Asset::​Memory->​new();​
 +            $asset->​add_chunk( $args{data} );
 +        } else {
 +            $self->​app->​log->​error('​You must provide "​data"​ or "​filepath"​ option'​);​
 +            return;
 +        }
 +        # Set response headers
 +        my $headers = $self->​res->​content->​headers();​
 +
 +        $filename = quote($filename);​ # quote the filename, per RFC 5987
 +        $filename = encode("​UTF-8",​ $filename);
 +
 +        $headers->​add( '​Content-Type',​ $content_type . '; name=' . $filename );
 +        $headers->​add( '​Content-Disposition',​ $content_disposition . '; filename='​ . $filename );
 +
 +        # Range, partially based on Mojolicious::​Static
 +        if ( my $range = $self->​req->​headers->​range ) {
 +            my $start = 0;
 +            my $size  = $asset->​size;​
 +            my $end   = $size - 1 >= 0 ? $size - 1 : 0;
 +
 +            # Check range
 +            if ( $range =~ m/​^bytes=(\d+)-(\d+)?/​ && $1 <= $end ) {
 +                $start = $1;
 +                $end = $2 if defined $2 && $2 <= $end;
 +
 +                $status = 206;
 +                $headers->​add( '​Content-Length'​ => $end - $start + 1 );
 +                $headers->​add( '​Content-Range' ​ => "bytes $start-$end/​$size"​ );
 +            } else {
 +                # Not satisfiable
 +                return $self->​rendered(416);​
 +            }
 +            # Set range for asset
 +            $asset->​start_range($start)->​end_range($end);​
 +        } else {
 +            $headers->​add( '​Content-Length'​ => $asset->​size );
 +        }
 +        # Stream content directly from file
 +        $self->​res->​content->​asset($asset);​
 +        return $self->​rendered($status);​
 +}
 +
 +sub sessionAuth {
 +    return 1 if shift->​session('​username'​);​
 +    return undef;
 +}
 +
 +sub checkPassword {
 +    my ($self, $username, $password) = @_;
 +
 +    return undef unless $username;
 +    return undef unless $password;
 +
 +    my $passwdFile = $self->​app->​config('​pwdfile'​);​
 +    do { 
 +        $self->​app->​log->​error("​Cannot read password file '​$passwdFile'"​);​
 +        return undef;
 +    } unless -r $passwdFile;​
 +    my $result = undef;
 +    eval {
 +        my $ht = Apache::​Htpasswd->​new({ passwdFile => $passwdFile,​ ReadOnly => 1 });
 +        $result = $ht->​htCheckPassword($username,​ $password);
 +    };
 +    do { $self->​app->​log->​debug("​Auth module error: $@"); return undef; } if $@;
 +
 +    return 1 if $result;
 +    $self->​app->​log->​info("​Bad auth from "​.$self->​tx->​remote_address);​
 +    return undef;
 +}
 +
 +sub login {
 +    my $self = shift;
 +
 +    return $self->​redirect_to('/​hello'​) if $self->​sessionAuth;​
 +
 +    my $username = $self->​req->​param('​username'​) || undef;
 +    my $password = $self->​req->​param('​password'​) || undef;
 +
 +    my $auth =  $self->​checkPassword($username,​ $password);
 +    if ($auth) {
 +        $self->​session(username => $username);
 +        return $self->​redirect_to('/​hello'​);​
 +    }
 +
 +    $self->​render(template => '​login',​ req => $self->​req);​
 +}
 +
 +sub dataList {
 +    my $self = shift;
 +    $self->​render(template => '​dataList'​);​
 +}
 +
 +sub dataDown {
 +    my $self = shift;
 +    my $name = $self->​req->​param('​name'​);​
 +
 +    return $self->​render(template => '​not_found.production'​) unless $name;
 +
 +    my $datadir = $self->​app->​config("​datadir"​);​
 +    my $file = "/​$datadir/​$name";​
 +
 +    return $self->​render(template => '​not_found.production'​) unless -r $file;
 +    return $self->​render(template => '​not_found.production'​) if -d $file;
 +    $self->​renderFile(filepath => "​$file"​);​
 +}
 +
 +
 +1;
 +#​-----------
 +#--- APP ---
 +#​-----------
 +package Recvi;
 +
 +use utf8;
 +use strict;
 +use warnings;
 +use Mojo::Base '​Mojolicious';​
 +
 +sub startup {
 +    my $self = shift;
 +}
 +
 +1;
 +#​------------
 +#--- MAIN ---
 +#​------------
 +use strict;
 +use warnings;
 +use utf8;
 +
 +use POSIX qw(setuid setgid);
 +use Mojo::​Server::​Prefork;​
 +use Mojo::​IOLoop::​Subprocess;​
 +use Mojo::Util qw(monkey_patch b64_encode b64_decode md5_sum getopt dumper);
 +use File::​Basename;​
 +use Sys::​Hostname qw(hostname);​
 +use File::​Basename qw(basename dirname);
 +use Apache::​Htpasswd;​
 +use Cwd qw(getcwd abs_path);
 +
 +
 +my $appfile = abs_path(__FILE__);​
 +my $appname = basename($appfile,​ "​.pl"​);​
 +$0 = $appfile;
 +
 +getopt
 +    '​h|help'​ => \my $help,
 +    '​4|ipv4listen=s'​ => \my $ipv4listen,​
 +    '​6|ipv6listen=s'​ => \my $ipv6listen,​
 +    '​c|config=s'​ => \my $conffile,
 +    '​p|pwdfile=s'​ => \my $pwdfile,
 +    '​d|datadir=s'​ => \my $datadir,
 +    '​l|logfile=s'​ => \my $logfile,
 +    '​i|pidfile=s'​ => \my $pidfile,
 +    '​v|loglevel=s'​ => \my $loglevel,
 +    '​f|nofork'​ => \my $nofork,
 +    '​u|user=s'​ => \my $user,
 +    '​g|group=s'​ => \my $group;
 +
 +
 +if ($help) {
 +    print qq(
 +Usage: app [OPTIONS]
 +
 +Options
 +    -h | --help ​                     This help
 +    -4 | --ipv4listen=address:​port ​     Listen address and port, defaults 127.0.0.1:​5100
 +    -6 | --ipv6listen=[address]:​port ​   Listen address and port, defaults [::1]:5100
 +
 +    -c | --config=path ​   Path to config file
 +    -p | --pwdfile=path ​  Path to user password file
 +    -d | --datadir=path ​   Path to application files 
 +    -l | --logfile=path ​  Path to log file
 +    -i | --pidfile=path ​  Path to process ID file
 +    -v | --loglevel=level ​ Verbose level: debug, info, warn, error, fatal
 +    -u | --user=user ​     System owner of process
 +    -g | --group=group ​   System group 
 +    -f | --nofork ​        Dont fork process, for debugging
 +All path option override option from configuration file
 +
 +    )."​\n";​
 +    exit 0;
 +}
 +
 +my $server = Mojo::​Server::​Prefork->​new;​
 +my $app = $server->​build_app('​Recvi'​);​
 +$app = $app->​controller_class('​Recvi::​Controller'​);​
 +
 +$app->​config(
 +    hostname => hostname,
 +    datadir => $datadir || "/​data",​
 +    listenIPv4 => $ipv4listen || "​0.0.0.0",​
 +    listenIPv6 => $ipv6listen || "​[::​]",​
 +    listenPort => "​3005",​
 +
 +    pwdfile => $pwdfile || "​@APP_CONFDIR@/​$appname.pw",​
 +    pidfile => $pidfile || "​@APP_RUNDIR@/​$appname.pid",​
 +    logfile => $logfile || "​@APP_LOGDIR@/​$appname.log",​
 +    conffile => $conffile || "​@APP_CONFDIR@/​$appname.conf",​
 +    maxrequestsize => 1024*1024*1024,​
 +    tlscert => "​@APP_CONFDIR@/​$appname.crt",​
 +    tlskey => "​@APP_CONFDIR@/​$appname.key",​
 +    appuser => $user || "​@APP_USER@",​
 +    appgroup => $group || "​@APP_GROUP@",​
 +    mode => '​production',​
 +    loglevel => $loglevel || '​info',​
 +    libdir => '​@APP_LIBDIR@',​
 +);
 +
 +$conffile = $app->​config('​conffile'​);​
 +do {
 +    $app->​log->​debug("​Load configuration from $conffile ");
 +    my $config = $app->​plugin( '​JSONConfig',​ { file => $conffile } );
 +} if -r $conffile;
 +
 +$app->​log->​level($app->​config('​loglevel'​));​
 +
 +my $tlscert = $app->​config('​tlscert'​);​
 +my $tlskey = $app->​config('​tlskey'​);​
 +$datadir = $app->​config('​datadir'​);​
 +my $rundir = dirname ($app->​config('​pidfile'​));​
 +my $logdir = dirname ($app->​config('​logfile'​));​
 +$pwdfile = $app->​config('​pwdfile'​);​
 +
 +do { print "​Error:​ Cannot read from data direcory $datadir\n";​ exit 1; } unless -r $datadir;
 +do { print "​Error:​ Cannot write to run direcory $rundir\n";​ exit 1; } unless -w $rundir;
 +do { print "​Error:​ Cannot write to log direcory $logdir\n";​ exit 1; } unless -w $logdir;
 +
 +do { print "​Error:​ Cannot read TLS certificate $tlscert\n";​ exit 1; } unless -r $tlscert;
 +do { print "​Error:​ Cannot read TLS key $tlskey\n";​ exit 1; } unless -r $tlskey;
 +do { print "​Error:​ Cannot read password file $pwdfile\n";​ exit 1; } unless -r $pwdfile;
 +
 +my $appUser = $app->​config('​appuser'​);​
 +my $appGroup = $app->​config('​appgroup'​);​
 +
 +my $appUID = getpwnam($appUser);​
 +my $appGID = getgrnam($appGroup);​
 +
 +do { print "​System user $appUser not exist.\n";​ exit 1; } unless $appUID;
 +do { print "​System group $appGroup not exist.\n";​ exit 1; } unless $appGID;
 +
 +$app->​moniker($appname);​
 +$app->​mode($app->​config("​mode"​));​
 +$app->​secrets(['​xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'​]);​
 +
 +$app->​hook(before_dispatch => sub {
 +        my $c = shift;
 +
 +        my $remoteIPaddr = $c->​tx->​remote_address;​
 +        my $method = $c->​req->​method;​
 +
 +        my $base = $c->​req->​url->​base->​to_string;​
 +        my $path = $c->​req->​url->​path->​to_string;​
 +
 +        my $loglevel = $c->​app->​log->​level;​
 +        unless ($loglevel eq '​debug'​) {
 +            my $url = $c->​req->​url->​to_abs->​to_string;​
 +            $c->​app->​log->​info("​$method $url from $remoteIPaddr"​);​
 +        }
 +        if ($loglevel eq '​debug'​) {
 +            my $url = $c->​req->​url->​to_abs->​to_string;​
 +            $c->​app->​log->​debug("​$method $url from $remoteIPaddr"​);​
 +        }
 +});
 +
 +$app->​static->​paths->​[0] = $app->​config('​libdir'​).'/​public';​
 +$app->​renderer->​paths->​[0] = $app->​config('​libdir'​).'/​templs';​
 +
 +my $r = $app->​routes;​
 +
 +$r->​add_condition(
 +    auth => sub {
 +        my ($route, $c, $captures, $hash) = @_;
 +        return 1 if $c->​sessionAuth;​
 +        return undef;
 +    }
 +);
 +
 +$r->​any('/​login'​)->​to('​Controller#​login'​);​
 +$r->​any('/​hello'​)->​over('​auth'​)->​to('​Controller#​hello'​);​
 +$r->​any('/​data/​list'​)->​over('​auth'​)->​to('​Controller#​dataList'​);​
 +$r->​any('/​data/​down'​)->​over('​auth'​)->​to('​Controller#​dataDown'​);​
 +
 +$app->​helper('​reply.not_found'​ => sub { 
 +        my $c = shift; ​
 +        return $c->​redirect_to('/​login'​) unless $c->​sessionAuth; ​
 +        $c->​render(template => '​not_found.production'​);​
 +});
 +
 +my $tlsParam .= '?';​
 +$tlsParam .= '​cert='​.$tlscert;​
 +$tlsParam .= '&​key='​.$tlskey;​
 +
 +my $listenPort = $app->​config('​listenPort'​);​
 +my $listenIPv4 = $app->​config('​listenIPv4'​);​
 +my $listenIPv6 = $app->​config('​listenIPv6'​);​
 +
 +$server->​listen([
 +    "​https://​$listenIPv4:​$listenPort$tlsParam",​
 +]);
 +
 +$server->​pid_file($app->​config('​pidfile'​));​
 +
 +$server->​heartbeat_interval(3);​
 +$server->​heartbeat_timeout(60);​
 +
 +unless ($nofork) {
 +    my $pid = fork;
 +    if ($pid == 0) {
 +        setuid($appUID) if $appUID;
 +        setgid($appGID) if $appGID;
 +        $app->​log(Mojo::​Log->​new( path => $app->​config('​logfile'​) ));
 +        open (my $STDOUT2, '>&',​ STDOUT); open (STDOUT, '>>',​ '/​dev/​null'​);​
 +        open (my $STDERR2, '>&',​ STDERR); open (STDERR, '>>',​ '/​dev/​null'​);​
 +        chdir($datadir);​
 +        local $SIG{HUP} = sub {
 +                $app->​log->​info('​Catch HUP signal'​); ​
 +                $app->​log(Mojo::​Log->​new(
 +                        path => $app->​config('​logfile'​),​
 +                        level => $app->​config('​loglevel'​),​
 +                ));
 +        };
 +        $server->​run;​
 +    }
 +} else {
 +    setuid($appUID) if $appUID;
 +    setgid($appGID) if $appGID;
 +    $server->​run;​
 +}
 +#EOF
 +</​code>​
 +
 +
 +<code html dataList.html.ep>​
 +%#
 +%# $Id$
 +%#
 +% layout '​default';​
 +% title '​RecVi';​
 +% use Mojo::Util qw(dumper);
 +% use File::​Basename;​
 +% use File::stat;
 +% use POSIX;
 +
 +% use utf8;
 +% use strict;
 +
 +% my $datadir = $self->​app->​config("​datadir"​);​
 +% unless (-d $datadir || -r $datadir) {
 +    <div class="​callout alert">​
 +         ​Cannot read data directory. Не могу прочитать каталог данных.
 +    </​div>​
 +%}
 +
 +% if (-d $datadir || -r $datadir) {
 +
 +<h5 class="​text-center">​
 +    Record list&​nbsp;​
 +    <a href="/​data/​list"><​i class="​fi-refresh"​ style="​font-size:​1.3rem;"></​i></​a>​
 +</h5>
 +
 +%     my $num = 1;
 +    <table id="​table"​ class="​display"​ >
 +        <​thead>​
 +        <tr>
 +    <​td>#</​td>​
 +    <​td>​date</​td>​
 +    <​td>​source</​td>​
 +    <​td>​dest</​td>​
 +    <​td>​duration</​td>​
 +    <​td>​size</​td>​
 +        </tr>
 +    </​thead>​
 +    <​tbody>​
 +%     ​opendir(my $dh, $datadir);
 +
 +%       while (my $name = readdir($dh)) {
 +%           next if ($name =~ m/^\./);
 +%           my $datafile = "​$datadir/​$name";​
 +%           next if -d $datafile;
 +%           next unless -r $datafile;
 +%           my $st = stat($datafile);​
 +%           my $size = $st->​size;​
 +%           next if $size < 101;
 +%           my $mtime = strftime("​%Y-%m-%d %H:%M:%S %Z", localtime($st->​mtime));​
 +%           my $rday = strftime("​%j",​ localtime($st->​mtime));​
 +%           my $cday = strftime("​%j",​ localtime(time));​
 +%           my $diff = $cday-$rday;​
 +%           next if ($diff > 45);
 +%           my ($Y, $M, $D, $h, $m, $s, $src, $dst) =
 +%              $name =~ /​rec-([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{2})-([0-9]{2})-([0-9]{2})-([0-9]{3})-([0-9]{1,​64})/​g;​
 +%           my $timestamp = "​$Y-$M-$D $h:​$m";​
 +
 +          <tr>
 +            <​td><​%= $num %></​td>​
 +            <​td><​a target="​_blank"​ href="/​data/​down?​name=<​%= $name %>"><​%= $timestamp %></​a></​td>​
 +            <​td><​%= $src %></​a></​td>​
 +            <​td><​%= $dst %></​a></​td>​
 +            <​td><​%= int($size/​16000+0.5) %></​td>​
 +            <​td><​%= $size %></​td>​
 +          </tr>
 +%       ​$num++;​
 +%       }
 +%       ​closedir $dh;
 +        </​tbody>​
 +    </​table>​
 +
 +    <​script>​
 +        $.extend(true,​ $.fn.dataTable.defaults,​ {
 +            "​searching":​ true,
 +            "​ordering":​ true
 +        } );
 +
 +        $(document).ready(function() {
 +            $('#​table'​).DataTable();​
 +        });
 +    </​script>​
 +% }
 +%#EOF
 +</​code>​
 +
 +<code m4 configure.ac>​
 +AC_INIT(recvi.pl)
 +AM_INIT_AUTOMAKE(recvi,​0.01)
 +AC_PREFIX_DEFAULT(/​usr/​local)
 +
 +PACKAGE=recvi
 +
 +AC_CHECK_PROG(HAVE_PERL,​ perl, true, false, /​usr/​local/​bin /usr/bin)
 +if test "​x$HAVE_PERL"​ = "​xfalse";​ then
 + AC_MSG_ERROR([Requested program perl not found])
 +fi
 +AC_SUBST(PERL,​ perl)
 +AC_PATH_PROG([PERL],​[perl])
 +
 +AC_PROG_INSTALL
 +
 +AC_CANONICAL_HOST
 +
 +case $host_os in
 +    *freebsd* )
 + AC_SUBST(ROOT_GROUP,​ "​wheel"​)
 + AM_CONDITIONAL(FREEBSD_OS,​ true)
 + AM_CONDITIONAL(LINUX_OS,​ false)
 + OSNAME=freebsd
 + ROOT_GROUP=wheel
 +        ;;
 +    *linux* )
 + AC_SUBST(ROOT_GROUP,​ "​root"​)
 + AM_CONDITIONAL(FREEBSD_OS,​ false)
 + AM_CONDITIONAL(LINUX_OS,​ true)
 + OSNAME=linux
 + ROOT_GROUP=root
 +        ;;
 +esac
 +
 +AC_ARG_WITH(confdir,​
 + AS_HELP_STRING([--with-confdir=PATH],​[set configuration dir to PATH (default: "​${ac_default_prefix}"/​etc/​${PACKAGE})]),​
 + [ if test ! -z "​$with_confdir"​ ; then
 + case $with_app_confdir in
 + /*)
 + APP_CONFDIR="​$with_confdir"​
 + ;;
 + *)
 +                                AC_MSG_ERROR(You must specify an absolute path to --with-confdir=PATH)
 + ;;
 + esac
 + else
 + APP_CONFDIR="​$ac_default_prefix/​etc/​${PACKAGE}"​
 + fi ],
 + [
 + APP_CONFDIR="​$ac_default_prefix/​etc/​${PACKAGE}"​
 + ])
 +
 +AC_DEFINE_UNQUOTED(APP_CONFDIR,​ "​$APP_CONFDIR",​ [location of configuration files for ${PACKAGE}])
 +AC_SUBST(APP_CONFDIR,​ "​$APP_CONFDIR"​)
 +
 +AC_ARG_WITH(logdir,​
 + AS_HELP_STRING([--with-logdir=PATH],​[set file path for source logdir (default: /​var/​log/​${PACKAGE}/​${PACKAGE}.log)]),​
 + [ if test ! -z "​$with_logdir"​ ; then
 + case $with_logdir in
 + /*)
 + APP_LOGDIR="​$with_logdir"​
 + ;;
 + *)
 +                                AC_MSG_ERROR(You must specify an absolute path to --with-logdir=PATH)
 + ;;
 + esac
 + else
 + APP_LOGDIR="/​var/​log/​${PACKAGE}"​
 + fi ],
 +
 + APP_LOGDIR="/​var/​log/​${PACKAGE}"​
 + ])
 +
 +AC_DEFINE_UNQUOTED(APP_LOGDIR,​ "​$APP_LOGDIR",​ [location of ${PACKAGE} logdir])
 +AC_SUBST(APP_LOGDIR,​ "​$APP_LOGDIR"​)
 +
 +AC_ARG_WITH(rundir,​
 + AS_HELP_STRING([--with-rundir=PATH],​[set file path for source rundir (default: /​var/​run/​${PACKAGE})]),​
 + [ if test ! -z "​$with_rundir"​ ; then
 + case $with_rundir in
 + /*)
 + APP_RUNDIR="​$with_rundir"​
 + ;;
 + *)
 +                                AC_MSG_ERROR(You must specify an absolute path to --with-rundir=PATH)
 + ;;
 + esac
 + else
 + APP_RUNDIR="/​var/​run/​${PACKAGE}"​
 + fi ],
 + [
 + APP_RUNDIR="/​var/​run/​${PACKAGE}"​
 + ])
 +
 +AC_DEFINE_UNQUOTED(APP_RUNDIR,​ "​$APP_RUNDIR",​ [location of pid file])
 +AC_SUBST(APP_RUNDIR,​ "​$APP_RUNDIR"​)
 +
 +dnl AC_ARG_WITH(dbdir,​
 +dnl AS_HELP_STRING([--with-dbdir=PATH],​[set file path for data files (default: "/​var/​db/​${PACKAGE}"​)]),​
 +dnl [ if test ! -z "​$with_dbdir"​ ; then
 +dnl case $with_dbdir in
 +dnl /*)
 +dnl APP_DBDIR="​$with_dbdir"​
 +dnl ;;
 +dnl *)
 +dnl                                 ​AC_MSG_ERROR(You must specify an absolute path to --with-dbdir=PATH)
 +dnl ;;
 +dnl esac
 +dnl else
 +dnl APP_DBDIR="/​var/​db/​${PACKAGE}"​
 +dnl fi ],
 +dnl [ APP_DBDIR="/​var/​db/​${PACKAGE}"​ ])
 +dnl AC_DEFINE_UNQUOTED(APP_DBDIR,​ "​$APP_DBDIR",​ [location of application data])
 +dnl AC_SUBST(APP_DBDIR,​ "​$APP_DBDIR"​)
 +
 +default_storedir="/​var/​pgstore"​
 +
 +AC_ARG_WITH(storedir,​
 + AS_HELP_STRING([--with-storedir=PATH],​[set data directory for pgstore (default: $default_storedir)]),​
 + [ if test ! -z "​$with_storedir"​ ; then
 + case $with_storedir in
 + /*)
 + PGSTORE_DATADIR="​$with_storedir"​
 + ;;
 + *)
 +                                AC_MSG_ERROR(You must specify an absolute path to --with-storedir=PATH)
 + ;;
 + esac
 + else
 + PGSTORE_DATADIR="​$default_storedir"​
 + fi ],
 + [
 + PGSTORE_DATADIR="​$default_storedir"​
 + ])
 +
 +AC_DEFINE_UNQUOTED(PGSTORE_DATADIR,​ "​$PGSTORE_DATADIR",​ [location of pgstore data dir])
 +AC_SUBST(PGSTORE_DATADIR,​ "​$PGSTORE_DATADIR"​)
 +
 +case $host_os in
 +    *freebsd* )
 + default_user="​www"​
 + default_group="​www"​
 +        ;;
 +    *linux* )
 + default_user="​www-data"​
 + default_group="​www-data"​
 +        ;;
 +esac
 +
 +AC_ARG_WITH(user,​
 + AS_HELP_STRING([--with-user=${PACKAGE}],​[set executing user name]),
 + [ if test ! -z "​$with_user"​ ; then
 + case $with_user in
 + ""​)
 + AC_MSG_ERROR(You must specify user name)
 + ;;
 + *)
 + APP_USER="​$with_user"​
 + ;;
 + esac
 + else
 + APP_USER="​$default_user"​
 + fi ],
 + [ APP_USER="​$default_user"​ ])
 +AC_DEFINE_UNQUOTED(APP_USER,​ "​$APP_USER",​ [effective user])
 +AC_SUBST(APP_USER,​ "​$APP_USER"​)
 +
 +AC_ARG_WITH(group,​
 + AS_HELP_STRING([--with-group=${PACKAGE}],​[set executing group name]),
 + [ if test ! -z "​$with_group"​ ; then
 + case $with_group in
 + ""​)
 + AC_MSG_ERROR(You must specify group name)
 + ;;
 + *)
 + APP_GROUP="​$with_group"​
 + ;;
 + esac
 + else
 + APP_GROUP="​$default_group"​
 + fi ],
 + [ APP_GROUP="​$default_group"​ ])
 +AC_DEFINE_UNQUOTED(APP_GROUP,​ "​$APP_GROUP",​ [effective group id])
 +AC_SUBST(APP_GROUP,​ "​$APP_GROUP"​)
 +
 +
 +AC_DEFINE_UNQUOTED(APP_LIBDIR,​ ${ac_default_prefix}/​share/​${PACKAGE},​ [application lib directory])
 +AC_SUBST(APP_LIBDIR,​ ${ac_default_prefix}/​share/​${PACKAGE})
 +
 +
 +AC_DEFUN([AC_PERL_MODULES],​[
 +ac_perl_modules="​$1"​
 +for ac_perl_module in $ac_perl_modules;​ do
 +AC_MSG_CHECKING(for perl module $ac_perl_module)
 +perl "​-M$ac_perl_module"​ -e exit > /dev/null 2>&1
 +if test $? -ne 0; then
 +    AC_MSG_RESULT(no);​
 +    AC_MSG_ERROR(You must install perl module $ac_perl_module)
 +  else
 +    AC_MSG_RESULT(ok);​
 +fi
 +done])
 +
 +AC_PERL_MODULES([
 +POSIX
 +Apache::​Htpasswd
 +DBI
 +File::​Basename
 +File::stat
 +Sys::​Hostname
 +Mojo::Base
 +Mojo::Home
 +Mojo::​IOLoop::​Subprocess
 +Mojo::JSON
 +Mojo::​Server::​Prefork
 +Mojo::​UserAgent
 +Mojo::Util
 +])
 +
 +AC_OUTPUT([
 +Makefile ​
 +recvi:​recvi.pl
 +rc.d/recvi
 +])
 +
 +dnl EOF
 +</​code>​
 +
 +<code make Makefile.am>​
 +#
 +# $Id: Makefile.am 633 2017-04-15 13:51:07Z ziggi $
 +#
 +AUTOMAKE_OPTIONS = foreign no-dependencies no-installinfo
 +
 +EXTRA_DIST = \
 + LICENSE
 +
 +install-data-hook:​
 +if FREEBSD_OS
 + chmod a+x $(DESTDIR)/​${etcdir}/​rc.d/​recvi
 +endif
 + $(INSTALL) -d -m 750 -o $(APP_USER) -g $(APP_GROUP) $(DESTDIR)$(APP_LOGDIR)
 + $(INSTALL) -d -m 750 -o $(APP_USER) -g $(APP_GROUP) $(DESTDIR)$(APP_RUNDIR)
 + for data in $(nobase_conf_DATA);​do \
 +   chmod 0644 $(DESTDIR)$(APP_CONFDIR)/​$$data;​ \
 + done
 +
 +if FREEBSD_OS
 +etcdir = @prefix@/​etc
 +nobase_etc_SCRIPTS = rc.d/recvi
 +endif
 +
 +sbin_SCRIPTS = recvi
 +
 +confdir = @APP_CONFDIR@
 +nobase_conf_DATA = \
 + recvi.pw.example \
 + recvi.crt.example \
 + recvi.conf.example \
 + recvi.key.example
 +
 +nobase_dist_pkgdata_DATA = \
 + public/​css/​app.css \
 + public/​css/​datatables.css \
 + public/​css/​datatables.min.css \
 + public/​css/​foundation-float.css \
 + public/​css/​foundation-float.min.css \
 + public/​css/​foundation-prototype.css \
 + public/​css/​foundation-prototype.min.css \
 + public/​css/​foundation-rtl.css \
 + public/​css/​foundation-rtl.min.css \
 + public/​css/​foundation.css \
 + public/​css/​foundation.min.css \
 + public/​favicon.ico \
 + public/​favicon.png \
 + public/​icons/​foundation-icons.css \
 + public/​icons/​foundation-icons.eot \
 + public/​icons/​foundation-icons.svg \
 + public/​icons/​foundation-icons.ttf \
 + public/​icons/​foundation-icons.woff \
 + public/​images/​sort_asc_disabled.png \
 + public/​images/​sort_asc.png \
 + public/​images/​sort_both.png \
 + public/​images/​sort_desc_disabled.png \
 + public/​images/​sort_desc.png \
 + public/​js/​app.js \
 + public/​js/​datatables.js \
 + public/​js/​datatables.min.js \
 + public/​js/​foundation.js \
 + public/​js/​foundation.min.js \
 + public/​js/​jquery.js \
 + public/​js/​jquery.min.js \
 + public/​js/​what-input.js \
 + \
 + templs/​dataList.html.ep \
 + templs/​exception.development.html.ep \
 + templs/​exception.production.html.ep \
 + templs/​hello.html.ep \
 + templs/​layouts/​default.html.ep \
 + templs/​not_found.development.html.ep \
 + templs/​not_found.production.html.ep \
 + templs/​login.html.ep
 +#EOF
 +</​code>​
 +
 +
 +<code html default.html.ep>​
 +
 +<​!doctype html>
 +<html class="​no-js"​ lang="​en"​ dir="​ltr">​
 +    <​head>​
 +        <meta charset="​utf-8">​
 +        <meta http-equiv="​x-ua-compatible"​ content="​ie=edge">​
 +        <meta name="​viewport"​ content="​width=device-width,​ initial-scale=1.0">​
 +        <​title><​%= title %></​title>​
 +        <link rel="​stylesheet"​ href="/​css/​foundation-float.min.css">​
 +        <link rel="​stylesheet"​ href="/​css/​datatables.css"/>​
 +        <link rel="​stylesheet"​ href="/​css/​app.css">​
 +
 +        <link rel="​stylesheet"​ href="/​icons/​foundation-icons.css">​
 +
 +        <script src="/​js/​jquery.min.js"></​script>​
 +        <script src="/​js/​datatables.min.js"></​script>​
 +        <script src="/​js/​foundation.min.js"></​script>​
 +
 +    </​head>​
 +
 +    <​body>​
 +
 +        <div class="​title-bar"​ data-responsive-toggle="​topbar-menu"​ data-hide-for="​medium">​
 +            <button class="​menu-icon"​ type="​button"​ data-toggle="​topbar-menu"></​button>​
 +            <div class="​title-bar-title">​Menu</​div>​
 +        </​div>​
 +
 +        <div class="​top-bar"​ id="​topbar-menu">​
 +            <div class="​top-bar-left">​
 +                <ul class="​dropdown menu" data-dropdown-menu>​
 +                    <li class="​menu-text">​RecVi</​li>​
 +                    <​li><​a href="/​data/​list">​Data</​a></​li>​
 +                </ul>
 +          </​div>​
 +        </​div>​
 +
 +        <div class="​row">&​nbsp;</​div>​
 +        <div class="​row">​
 +<!- end of head template ->
 +
 +<%= content %>
 +
 +<!- begin of tail template ->
 +            </​div>​
 +        </​div>​
 +        <hr/>
 +        <div class="​row">​
 +            <p class="​text-center"><​small>​Made by <a href="​http://​wiki.unix7.org">​Borodin Oleg</​a></​small></​p>​
 +        </​div>​
 +
 +        <script src="/​js/​app.js"></​script>​
 +    </​body>​
 +</​html>​
 +<!- end of tail template ->
 +<!- EOF ->
 +</​code>​
 +
 +<code html login.html.ep>​
 +
 +%#
 +%# $Id: login.html.ep 634 2017-04-15 13:55:49Z ziggi $
 +%#
 +<html class="​no-js"​ lang="​en"​ dir="​ltr">​
 +    <​head>​
 +        <meta charset="​utf-8">​
 +        <meta http-equiv="​x-ua-compatible"​ content="​ie=edge">​
 +        <meta name="​viewport"​ content="​width=device-width,​ initial-scale=1.0">​
 +        <​title>​Login</​title>​
 +        <link rel="​stylesheet"​ href="/​css/​foundation-float.min.css">​
 +        <link rel="​stylesheet"​ href="/​css/​app.css">​
 +
 +        <link rel="​stylesheet"​ href="/​icons/​foundation-icons.css">​
 +
 +        <script src="/​js/​jquery.min.js"></​script>​
 +        <script src="/​js/​foundation.min.js"></​script>​
 +    </​head>​
 +    <​body>​
 +        <div class="​text-center"​ style="​margin:​5em 5em 5em 5em;">​
 +            <form accept-charset="​UTF-8"​ method="​post"​ action="/​login">​
 +                <div class="​row column">​
 +                    <h4 class="​text-center">​Login with your username</​h4>​
 +                    <​label>​Username
 +                        <input type="​text"​ name="​username"​ placeholder="​somename"​ />
 +                    </​label>​
 +                    <​label>​Password
 +                        <input type="​password"​ name="​password"​ placeholder="​password"​ />
 +                    </​label>​
 +                    <p class="​text-center">​
 +                        <button type="​submit"​ class="​button">​LogIn</​button>​
 +                    </p>
 +            </​form>​
 +        </​div>​
 +
 +        <hr/>
 +        <div class="​row">​
 +            <p class="​text-center"><​small>​Made by <a href="​http://​wiki.unix7.org">​Borodin Oleg</​a></​small></​p>​
 +        </​div>​
 +
 +        <script src="/​js/​app.js"></​script>​
 +    </​body>​
 +</​html>​
 +<!- EOF ->
 +%# EOF
 +</​code>​
 +
 +----
 +[<>]