User Tools

Site Tools


PGdumper current code

The work still in progress

pgagent.pl

pgagent.pl
#!@PERL@
 
#------------
#--- AUTH ---
#------------
 
package PGagent::BasicAuth;
 
use strict;
use warnings;
use POSIX qw(getpid setuid setgid geteuid getegid);
use Cwd qw(cwd getcwd chdir);
use Mojo::Util qw(md5_sum b64_decode dumper);
use Apache::Htpasswd;
 
sub new {
    my ($class, $pwdfile) = @_;
    my $self = {
        pwdfile => $pwdfile,
        errstr => undef
    };
    bless $self, $class;
    return $self;
}
 
sub pwdfile {
    my ($self, $pwdfile) = @_;
    return $self->{pwdfile} unless $pwdfile;
    $self->{pwdfile} = $pwdfile if $pwdfile;
    $self;
}
 
sub auth {
    my ($self, $authstr) = @_;
    return undef unless $authstr;
 
    my $hash = $self->split($authstr);
    return undef unless $hash;
    return undef unless -r $self->{pwdfile};
 
    my $res = undef;
    eval {
        my $ht = Apache::Htpasswd->new( { passwdFile => $self->pwdfile, ReadOnly => 1 } );
        $res = $ht->htCheckPassword(
                            $hash->{username},
                            $hash->{password}
        );
    };
    return undef if $@;
    $res;
}
 
sub username {
    my ($self, $authstr) = @_;
    return undef unless $authstr;
    my $hash = $self->split($authstr);
    return undef unless $hash;
    $hash->{username} if $hash;
}
 
sub split {
    my ($self, $authstr) = @_;
    return undef unless $authstr;
 
    my ($type, $enc) = split /\s+/, $authstr;
    return undef unless ($type eq 'Basic' && $enc);
 
    my ($username, $password) = split /:/, b64_decode($enc);
    return undef unless ($username && $password);
 
    { username => $username, password => $password };
}
 
1;
 
 
#--------------
#--- DAEMON ---
#--------------
 
package PGagent::Daemon;
 
use strict;
use warnings;
use POSIX qw(getpid setuid setgid geteuid getegid);
use Cwd qw(cwd getcwd chdir);
use Mojo::Util qw(dumper);
 
sub new {
    my $class = shift;
    my $self = {
        pid => undef
    };
    bless $self, $class;
    return $self;
}
 
sub fork {
    my ($self, $user, $group) = shift;
    my $pid = fork;
    if ($pid > 0) {
        exit;
    }
    chdir("/");
    open(my $stdout, '>&', STDOUT); 
    open(my $stderr, '>&', STDERR);
    open(STDOUT, '>>', '/dev/null');
    open(STDERR, '>>', '/dev/null');
    $self->pid(getpid);
    $self->pid;
}
 
sub pid {
    my ($self, $pid) = @_;
    return $self->{pid} unless $pid;
    $self->{pid} = $pid if $pid;
    $self;
}
 
1;
 
#----------
#--- DB ---
#----------
 
package PGagent::DB;
 
use strict;
use warnings;
use DBI;
 
sub new {
    my ($class, %args) = @_;
    my $self = {
        hostname => $args{hostname} || 'localhost',
        username => $args{username} || 'postgres',
        password => $args{password} || 'password',
        database => $args{database} || 'postgres',
        engine => 'Pg',
        error => ''
    };
    bless $self, $class;
    return $self;
}
 
sub username {
    my ($self, $username) = @_; 
    return $self->{username} unless $username;
    $self->{username} = $username;
    $self;
}
 
sub password {
    my ($self, $password) = @_; 
    return $self->{password} unless $password;
    $self->{password} = $password;
    $self;
}
 
sub hostname {
    my ($self, $hostname) = @_; 
    return $self->{hostname} unless $hostname;
    $self->{hostname} = $hostname;
    $self;
}
 
sub database {
    my ($self, $database) = @_; 
    return $self->{database} unless $database;
    $self->{database} = $database;
    $self;
}
 
sub error {
    my ($self, $error) = @_; 
    return $self->{error} unless $error;
    $self->{error} = $error;
    $self;
}
 
sub engine {
    my ($self, $engine) = @_; 
    return $self->{engine} unless $engine;
    $self->{engine} = $engine;
    $self;
}
 
 
sub exec {
    my ($self, $query) = @_;
    return undef unless $query;
 
    my $dsn = 'dbi:'.$self->engine.
                ':dbname='.$self->database.
                ';host='.$self->hostname;
    my $dbi;
    eval {
        $dbi = DBI->connect($dsn, $self->username, $self->password, { 
            RaiseError => 1,
            PrintError => 0,
            AutoCommit => 1 
        });
    };
    $self->error($@);
    return undef if $@;
 
    my $sth;
    eval {
        $sth = $dbi->prepare($query);
    };
    $self->error($@);
    return undef if $@;
 
    my $rows = $sth->execute;
    my @list;
 
    while (my $row = $sth->fetchrow_hashref) {
        push @list, $row;
    }
    $sth->finish;
    $dbi->disconnect;
    \@list;
}
 
sub do {
    my ($self, $query) = @_;
    return undef unless $query;
    my $dsn = 'dbi:'.$self->engine.
                ':dbname='.$self->database.
                ';host='.$self->hostname;
    my $dbi;
    eval {
        $dbi = DBI->connect($dsn, $self->username, $self->password, { 
            RaiseError => 1,
            PrintError => 0,
            AutoCommit => 1 
        });
    };
    $self->error($@);
    return undef if $@;
    my $rows;
    eval {
        $rows = $dbi->do($query) or return undef;
    };
    $self->error($@);
    return undef if $@;
 
    $dbi->disconnect;
    $rows*1;
}
 
1;
 
#-------------
#--- MODEL ---
#-------------
 
package PGagent::Model;
 
use strict;
use warnings;
use File::stat;
use Data::Dumper;
use DBI;
use Mojo::UserAgent;
use Mojo::Util qw(dumper);
use Mojo::JSON qw(encode_json decode_json true false);
use POSIX;
use Socket;
 
#sub new {
#    my ($class, $app, $dbhost, $dbuser, $dbpwd) = @_;
#    my $self = {
#        app => $app,
#        db => PGagent::DB->new(
#                        hostname => $dbhost,
#                        username => $dbuser,
#                        password => $dbpwd,
#                        database => 'postgres',
#        )
#    };
#    bless $self, $class;
#    return $self;
#}
 
sub new {
    my ($class, $app, $db) = @_;
    my $self = {
        app => $app,
        db => $db
    };
    bless $self, $class;
    return $self;
}
 
sub app {
    shift->{app}; 
}
 
sub db {
    shift->{db}; 
}
 
sub log {
    shift->app->log; 
}
 
sub hello {
    "hello!";
}
 
 
sub pgpasswd {
    shift->db->password;
}
 
sub pguser {
    shift->db->username;
}
 
sub pghost {
    shift->db->hostname;
}
 
sub store_alive {
    my ($self, $hostname) = @_;
    return undef unless $hostname;
 
    my $ua = Mojo::UserAgent->new;
    my $tx = $ua->get("https://$hostname:3002/hello");
    $ua = $ua->connect_timeout(5)->request_timeout(5);;
 
    my $res;
    eval { $res = decode_json($tx->result->body); };
    return 1 if $res->{'message'} eq 'hello';
    return undef;
}
 
sub store_put {
    my ($self, $datafile, $storename, $storeuser, $storepwd) = @_;
    my $ua = Mojo::UserAgent->new(max_redirects => 5);
    $ua = $ua->connect_timeout(15)->request_timeout(15);;
 
    return undef unless $datafile;
    return undef unless $storename;
 
    return undef unless -f $datafile;
    return undef unless -r $datafile;
    return undef unless $self->store_alive($storename);
 
    $self->app->log->info("store_put: Start upload $datafile to store $storename");
    my $tx = $ua->post("https://$storeuser:$storepwd\@$storename:3002/data/put" =>
                            form => { data => { file => $datafile }} );
    $self->app->log->info("store_put: End upload $datafile to store $storename");
 
    my $res;
    eval { $res = $tx->result; };
    return $res->body if $res;
    return undef;
}
 
sub store_get {
    my ($self, $dataset, $storename, $storeuser, $storepwd) = @_;
    return undef unless $dataset;
    return undef unless $storename;
    return undef unless $self->store_alive($storename);
 
    $storeuser = 'undef' unless $storeuser;
    $storepwd = 'undef' unless $storepwd;
 
    my $tmpdir = $self->app->config('tmpdir');
 
    return undef unless -d $tmpdir;
    return undef unless -w $tmpdir;
 
    my $datafile = "$tmpdir/rest".."$dataset";
    $self->app->log->info("store_get: Start download $dataset from store $storename to $datafile");
 
    my $ua = Mojo::UserAgent->new(max_redirects => 5);
    $ua = $ua->connect_timeout(30)->request_timeout(30);;
 
    my $tx = $ua->get("https://$storeuser:$storepwd\@$storename:3002/data/get?dataname=$dataset");
    $tx->result->content->asset->move_to($datafile);
    $self->app->log->info("store_get: End download $dataset from store $storename");
 
    return $datafile if -r $datafile;
    unlink $datafile; return undef;
}
 
 
#-----------------
#--- DB MODEL ----
#-----------------
 
sub db_exist {
    my ($self, $dbname) = @_;
    return undef unless $dbname;
    my $dblist = $self->db_list;
    foreach my $db (@$dblist) {
        return 1 if $db->{"name"} eq $dbname;
    }
    return undef;
}
 
sub db_size {
    my ($self, $dbname) = @_;
    return undef unless $dbname;
    return undef unless $self->db_exist($dbname);
 
    my $query = "select pg_database_size('$dbname') as size;";
    my $res = $self->db->exec($query);
    $res->[0]{size};
}
 
sub db_list {
    my $self = shift;
    my $query = "select d.datname as name,
                        pg_database_size(d.datname) as size,
                        u.usename as owner,
                        s.numbackends as numbackends
                 from pg_database d, pg_user u, pg_stat_database s
                 where d.datdba = u.usesysid and d.datname = s.datname
                 order by d.datname;";
    $self->db->exec($query);
}
 
sub db_create {
    my ($self, $dbname) = @_;
    return undef unless $dbname;
    return undef if $self->db_exist($dbname);
    my $query = "create database $dbname";
    $self->db->do($query);
    $self->db_exist($dbname);
}
 
sub db_drop {
    my ($self, $dbname) = @_;
    return undef unless $dbname;
    return undef unless $self->db_exist($dbname);
    my $query = "drop database $dbname";
    $self->db->do($query);
    return undef if $self->db_exist($dbname);
    $dbname;
}
 
sub db_rename {
    my ($self, $dbname, $newname) = @_;
    return undef unless $dbname;
    return undef unless $newname;
    return undef unless $self->db_exist($dbname);
 
    my $query = "alter database $dbname rename to $newname;";
    $self->db->do($query);
    $self->db_exist($newname);
}
 
sub db_copy {
    my ($self, $dbname, $newname) = @_;
    return undef unless $dbname;
    return undef unless $newname;
    return undef unless $self->db_exist($dbname);
    return undef if $self->db_exist($newname);
 
    my $query = "create database $newname template $dbname;";
    $self->db->do($query);
    $self->db_exist($newname);
}
 
 
sub db_dump {
    my ($self, $dbname, $hostname, $tmpdir) = @_;
 
    return undef unless $dbname;
    return undef unless $self->db_exist($dbname);
 
    return undef unless -d $tmpdir;
    return undef unless -w $tmpdir;
 
    my $timestamp = strftime("%Y%m%d-%H%M%S-%Z", localtime(time));
    my $dbdump = "$tmpdir/$dbname--$timestamp--$hostname.sqlz";
 
    my $password = $self->pgpasswd;
    my $pghost = $self->pghost;
    my $username = $self->pguser;
 
    my $out = qx/PGPASSWORD=$password pg_dump -h $pghost -U $username -Fc -f $dbdump $dbname 2>&1/;
    my $retcode = $?;
 
    return $dbdump if $retcode == 0;
    undef;
}
 
sub db_restore {
    my ($self, $filename, $dbname) = @_;
    return undef unless $dbname;
    return undef unless $filename;
    return undef unless -r $filename;
    return undef unless -s $filename;
 
    return undef if $self->db_exist($dbname);
 
    return undef unless $self->db_create($dbname);
 
    my $password = $self->pgpasswd;
    my $pghost = $self->pghost;
    my $username = $self->pguser;
 
    my $out = qx/PGPASSWORD=$password pg_restore -j 4 -h $pghost -U $username -Fc -d $dbname $filename 2>&1/;
    my $retcode = $?;
 
    if ($retcode > 1) {
        $self->db_drop($dbname) if $self->db_exist($dbname);
        return undef;
    }
    $dbname;
}
 
sub db_owner {
    my ($self, $dbname) = @_;
    return undef unless $dbname;
    return undef unless $self->db_exist($dbname);
    my $query = "select u.usename as username, d.datname as dbname 
                        from pg_database d, pg_user u 
                        where d.datdba = u.usesysid and d.datname = '$dbname' limit 1";
    $self->db->exec($query);
}
 
 
#-------------------
#--- ROLE MODEL ----
#-------------------
 
 
sub role_list {
    my $self = shift;
    my $query = "select usename as rolename from pg_user order by rolename";
    $self->db->exec($query);
}
 
sub role_exist {
    my ($self, $role) = @_;
    return undef unless $role;
    my $list = $self->role_list;
    foreach my $name (@{$self->role_list}) {
        return $name->{rolename} if $name->{rolename} eq $role;
    }
    return undef;
}
 
sub role_create {
    my ($self, $rolename, $password) = @_;
    return undef unless $password;
    return undef unless $rolename;
    return undef unless $self->role_exist($rolename);
 
    my $query = "create user '$rolename' encrypted password '$password';";
    $self->db->do($query);
    $self->role_exist($rolename);
}
 
sub role_drop {
    my ($self, $rolename) = @_;
    return undef unless $rolename;
    return undef unless $self->role_exist($rolename);
 
    my $query = "drop user '$rolename';";
    $self->db->do($query);
    return undef if $self->role_exist($rolename);
    $rolename;
}
 
sub role_password {
    my ($self, $rolename, $password) = @_;
    return undef unless $password;
    return undef unless $rolename;
    return undef unless $self->role_exist($rolename);
 
    my $query = "alter role $rolename encrypted password '$password'";
    $self->db->do($query);
    $self->role_exist($rolename);
}
 
1;
 
#------------------
#--- CONTROLLER ---
#------------------
 
package PGagent::Controller;
 
use utf8;
use strict;
use warnings;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::JSON qw(encode_json decode_json true false);
use Mojo::Util qw(dumper);
use Mojo::IOLoop::Subprocess;
use Apache::Htpasswd;
 
sub hello {
    my $self = shift;
    $self->render(json => { message => 'hello', success => true });
}
 
sub db_list {
    my $self = shift;
    $self->render(json => { dblist => $self->app->model->db_list, success => true });
}
 
sub db_size {
    my $self = shift;
    my $dbname = $self->req->param('dbname');
    return $self->render(json => { success => false, dbname => $dbname }) unless $dbname;
 
    my $dbsize = $self->app->model->db_size($dbname);
    $self->render(json => { name => $dbname, size => $dbsize });
}
 
sub db_create {
    my $self = shift;
    my $dbname = $self->req->param('dbname');
    return $self->render(json => { success => false }) unless $dbname;
 
    my $res = $self->app->model->db_create($dbname);
 
    return $self->render(json => { success => true }) if $res;
    $self->render(json => { success => false });
}
 
sub db_drop {
    my $self = shift;
    my $dbname = $self->req->param('dbname');
 
    return $self->render(json => { success => false }) unless $dbname;
 
    my $res = $self->app->model->db_drop($dbname);
 
    return $self->render(json => { success => false }) unless $res;
    return $self->render(json => { success => true });
}
 
sub db_rename {
    my $self = shift;
    my $dbname = $self->req->param('dbname');
    my $newname = $self->req->param('newname');
 
    return $self->render(json => { success => false }) unless $dbname;
    return $self->render(json => { success => false }) unless $newname;
 
    my $res = $self->app->model->db_rename($dbname, $newname);
 
    return $self->render(json => { success => true }) if $res;
    return $self->render(json => { success => false });
}
 
sub db_copy {
    my $self = shift;
    my $dbname = $self->req->param('dbname');
    my $newname = $self->req->param('newname');
    return $self->render(json => { success => false }) unless $dbname;
    return $self->render(json => { success => false }) unless $newname;
 
    my $res = $self->app->model->db_copy($dbname, $newname);
    my $dbsize = $self->app->model->db_size($dbname) if $res;
 
    return $self->render(json => { success => true, dbsize => $dbsize}) if $res;
    return $self->render(json => { success => false});
}
 
sub db_exist {
    my $self = shift;
    my $dbname = $self->req->param('dbname');
    return $self->render(
            json => { success => false, dbname => '' }
    ) unless $dbname;
    my $res = $self->app->model->db_exist($dbname);
    my $dbsize = $self->app->model->db_size($dbname) if $res;
    return $self->render(
            json => { success => true, dbname => $dbname, dbsize => $dbsize}
    ) if $res;
    return $self->render(
            json => { success => false, dbname => $dbname }
    );
}
 
sub master_cb {
    my ($self, $master, $job_id, $magic, $status, $error, $result) = @_;
 
    return undef unless $master;
    return undef unless $job_id;
    return undef unless $magic;
    return undef unless $status;
 
    $error = 'noerr' unless $error;
    $result = 'success' unless $result;
 
    my $ua = Mojo::UserAgent->new;
    $ua = $ua->connect_timeout(15)->request_timeout(15);;
 
    my $param;
    $param .= "&master=$master";
    $param .= "&jobid=$job_id";
    $param .= "&magic=$magic";
    $param .= "&status=$status";
    $param .= "&success=true";
 
    my $url = "https://$master:3003/agent/job/cb?$param";
    $self->app->log->debug("master_cb: master callback $url");
 
    my $tx = $ua->get($url);
    my $body;
    eval { $body = $tx->result->body };
    my $res;
    eval { $res = decode_json($body); };
    return $res if $res;
    return undef;
}
 
 
sub db_dump {
    my $self = shift;
    my $dbname = $self->req->param('dbname');
 
    my $storename = $self->req->param('store');
    my $storelogin = $self->req->param('storelogin');
    my $storepwd = $self->req->param('storepwd');
 
    my $master = $self->req->param('master');
    my $job_id = $self->req->param('jobid');
    my $magic = $self->req->param('magic');
 
    my $hostname = $self->app->config('hostname');
    my $tmpdir = $self->app->config('tmpdir');
 
    return $self->render(
            json => { success => false, dbname => '' }
    ) unless $dbname;
    return $self->render(
            json => { success => false, dbname => $dbname }
    ) unless $self->app->model->db_exist($dbname);
 
    my $subprocess = Mojo::IOLoop::Subprocess->new;
 
    $self->app->log->info("db_dump: Begin dump database $dbname in subprocess"); 
    $subprocess->run(
        sub {
            my $subprocess = shift;
 
            $self->app->log->info("db_dump subprocess: Begin dump database $dbname"); 
            my $filename = $self->app->model->db_dump($dbname, $hostname, $tmpdir);
            $self->app->log->info("db_dump subprocess: Done dump database $dbname, result is file $filename"); 
 
            unless ($filename) { 
                unlink $filename; 
                $self->master_cb($master, $job_id, $magic, "dumperr", "dumperr", 'mistake');
                return undef;
            }
 
            my $res_cb = $self->master_cb($master, $job_id, $magic, "dumped", "noerr", 'success');
 
            $self->app->log->info("db_dump subprocess: Begin store database $dbname"); 
            my $store_res = $self->app->model->store_put($filename, $storename, $storelogin, $storepwd);
            $self->app->log->info("db_dump subprocess: Done store database $dbname with store responce $store_res"); 
 
            unless ($store_res) {
                unlink $filename;
                $self->master_cb($master, $job_id, $magic, "storeerr", "storeerr", 'mistake');
                return undef; 
            }
 
            $self->master_cb($master, $job_id, $magic, "stored", "noerr", 'success');
            unlink $filename;
            1;
        },
        sub {
            my ($subprocess, $err, @results) = @_;
            my $pid = $subprocess->pid;
            $self->app->log->info("db_dump: End dump subprocess $pid for dump database $dbname");
        }
    );
    $subprocess->ioloop->start unless $subprocess->ioloop->is_running;
    $self->render(json => { success => true, dbname => $dbname } );
}
 
sub db_restore {
    my $self = shift;
 
 
    my $req = $self->req;
 
    my $dataname = $req->param('dataname');
    my $storename = $req->param('store');
    my $storelogin = $req->param('storelogin');
    my $storepwd = $req->param('storepwd');
 
    my $newname = $req->param('newname');
 
    my $master = $req->param('master');
    my $job_id = $req->param('jobid');
    my $magic = $req->param('magic');
 
    my $m = $self->app->model;
 
    return $self->render(json => { success => false }) unless ($dataname and $storename and $storelogin);
    return $self->render(json => { success => false }) unless ($storepwd and $newname and $job_id and $master and $magic);
    return $self->render(json => { success => false }) unless $m->store_alive($storename);
 
    my $subprocess = Mojo::IOLoop::Subprocess->new;
 
    $subprocess->run(
        sub {
            my $subprocess = shift;
            $self->master_cb($master, $job_id, $magic, "dataget", "noerr", 'success');
 
            my $datafile = $m->store_get($dataname, $storename, $storelogin, $storepwd);
 
            unless ($datafile || -s $datafile) {
                unlink $dataname;
                $self->master_cb($master, $job_id, $magic, "geterr", "geterr", 'mistake');
                return undef;
            }
 
            $self->master_cb($master, $job_id, $magic, "datagot", "noerr", 'success');
 
            my $newdbname = $m->db_restore($datafile, $newname);
 
            unless ($newdbname) {
                unlink $dataname;
                $self->master_cb($master, $job_id, $magic, "resterr", "resterr", 'mistake');
                return undef; 
            };
 
            $self->master_cb($master, $job_id, $magic, "done", "noerr", 'success');
            1;
 
        },
        sub {
            my ($subprocess, $err, @results) = @_;
            my $pid = $subprocess->pid;
            $self->app->log->info("db_restore: End restore subprocess $pid for dataset $dataname");
        }
    );
    $subprocess->ioloop->start unless $subprocess->ioloop->is_running;
 
    $self->render(json => { success => true });
}
 
sub role_list {
    my $self = shift;
    $self->render(json => { success => true, role => $self->app->model->role_list });
}
 
sub role_exist {
    my $self = shift;
    my $rolename = $self->req->param('rolename');
 
    return $self->render(json => { success => false, rolename => undef })  unless $rolename;
 
    my $res = $self->app->model->role_exist($rolename);
 
    return $self->render(json => { success => true, rolename => $rolename }) if $res;
    $self->render(json => { success => false, rolename => $rolename });
}
 
sub role_password {
    my $self = shift;
    my $rolename = $self->req->param('rolename');
 
    return $self->render(json => { success => false, rolename => undef }) unless $rolename;
    # dummy
    $self->render(json => { success => true, rolename => $rolename });
}
 
sub role_create {
    my $self = shift;
    my $rolename = $self->req->param('rolename');
    return $self->render(json => { success => false, rolename => undef })  unless $rolename;
    $self->render(json => { success => true, rolename => $rolename });
}
 
sub role_drop {
    my $self = shift;
    my $rolename = $self->req->param('rolename');
 
    return $self->render(json => { success => false, rolename => undef })  unless $rolename;
    $self->render(json => { success => true, rolename => $rolename });
}
 
1;
 
#-----------
#--- APP ---
#-----------
 
package PGagent;
 
use strict;
use warnings;
use Mojo::Base 'Mojolicious';
 
sub startup {
    my $self = shift;
}
 
1;
 
#------------
#--- MAIN ---
#------------
 
use strict;
use warnings;
 
use POSIX qw(setuid setgid tzset tzname strftime);
use Mojo::Server::Prefork;
use Mojo::IOLoop::Subprocess;
use Mojo::Util qw(md5_sum b64_decode getopt dumper);
use Sys::Hostname qw(hostname);
use File::Basename qw(basename dirname);
use Apache::Htpasswd;
use Cwd qw(getcwd abs_path);
use EV;
 
my $appname = 'pgdumper';
 
#--------------
#--- GETOPT ---
#--------------
 
getopt
    'h|help' => \my $help,
    'c|config=s' => \my $conffile,
    '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
    -c | --config=path    Path to config file
    -u | --user=user      System owner of process
    -g | --group=group    System group 
    -f | --nofork         Dont fork process
 
The options override options from configuration file
    )."\n";
    exit 0;
}
 
#------------------
#--- APP CONFIG ---
#------------------
 
my $server = Mojo::Server::Prefork->new;
my $app = $server->build_app('PGagent');
$app = $app->controller_class('PGagent::Controller');
 
$app->secrets(['6d578e43ba88260e0375a1a35fd7954b']);
$app->static->paths(['@APP_LIBDIR@/public']);
$app->renderer->paths(['@APP_LIBDIR@/templs']);
 
$app->config(conffile => $conffile || '@APP_CONFDIR@/pgagent.conf');
$app->config(pwdfile => '@APP_CONFDIR@/pgagent.pw');
$app->config(logfile => '@APP_LOGDIR@/pgagent.log');
$app->config(loglevel => 'debug');
$app->config(pidfile => '@APP_RUNDIR@/pgagent.pid');
$app->config(crtfile => '@APP_CONFDIR@/pgagent.crt');
$app->config(keyfile => '@APP_CONFDIR@/pgagent.key');
 
$app->config(user => $user || '@APP_USER@');
$app->config(group => $group || '@APP_GROUP@');
 
$app->config(listenaddr4 => '0.0.0.0');
$app->config(listenaddr6 => '[::]');
$app->config(listenport => '3001');
 
$app->config(tmpdir => '/tmp');
 
$app->config(pghost => '127.0.0.1');
$app->config(pguser => 'postgres');
$app->config(pgpwd => 'password');
 
$app->config(hostname => hostname);
 
 
if (-r $app->config('conffile')) {
    $app->log->debug("Load configuration from ".$app->config('conffile'));
    $app->plugin('JSONConfig', { file => $app->config('conffile') });
}
 
#---------------
#--- HELPERS ---
#---------------
 
$app->helper('reply.exception' => sub { my $c = shift; return $c->rendered(404); });
$app->helper('reply.not_found' => sub { my $c = shift; return $c->rendered(404); });
 
 
$app->helper(
    model => sub {
        my $db = PGagent::DB->new(
                        hostname => $app->config("pghost"),
                        username => $app->config("pguser"),
                        password => $app->config("pgpwd"),
                        database => 'postgres'
        );
        state $model = PGagent::Model->new($app, $db);
    }
);
 
 
#--------------
#--- ROUTES ---
#--------------
 
my $r = $app->routes;
 
$r->add_condition(
    auth => sub {
        my ($route, $c) = @_;
        my $log = $c->app->log;
        my $authstr = $c->req->headers->authorization;
        my $pwdfile = $c->app->config('pwdfile');
 
        my $a = PGagent::BasicAuth->new($pwdfile);
 
        $log->info("Try auth user ". $a->username($authstr));
        $a->auth($authstr);
 
    }
);
 
$r->any('/hello')->to('controller#hello');
$r->any('/conf/dump')->over('auth')->to('controller#conf_dump');
 
$r->any('/db/list')     ->over('auth')->to('controller#db_list');
$r->any('/db/create')   ->over('auth')->to('controller#db_create');
$r->any('/db/drop')     ->over('auth')->to('controller#db_drop');
$r->any('/db/rename')   ->over('auth')->to('controller#db_rename');
$r->any('/db/copy')     ->over('auth')->to('controller#db_copy');
$r->any('/db/size')     ->over('auth')->to('controller#db_size');
$r->any('/db/dump')     ->over('auth')->to('controller#db_dump');
$r->any('/db/restore')  ->over('auth')->to('controller#db_restore');
$r->any('/db/exist')    ->over('auth')->to('controller#db_exist');
$r->any('/db/owner')    ->over('auth')->to('controller#db_owner');
 
 
$r->any('/role/list')   ->over('auth')->to('controller#role_list');
$r->any('/role/exist')  ->over('auth')->to('controller#role_exist');
$r->any('/role/password')->over('auth')->to('controller#role_password');
$r->any('/role/create') ->over('auth')->to('controller#role_create');
$r->any('/role/drop')   ->over('auth')->to('controller#role_drop');
 
 
#----------------
#--- LISTENER ---
#----------------
 
my $tls = '?';
$tls .= 'cert='.$app->config('crtfile');
$tls .= '&key='.$app->config('keyfile');
 
my $listen4;
if ($app->config('listenaddr4')) {
    $listen4 = "https://";
    $listen4 .= $app->config('listenaddr4').':'.$app->config('listenport');
    $listen4 .= $tls;
}
 
my $listen6;
if ($app->config('listenaddr6')) {
    $listen6 = "https://";
    $listen6 .= $app->config('listenaddr6').':'.$app->config('listenport');
    $listen6 .= $tls;
}
 
my @listen;
push @listen, $listen4 if $listen4;
push @listen, $listen6 if $listen6;
 
$server->listen(\@listen);
$server->heartbeat_interval(3);
$server->heartbeat_timeout(60);
 
#--------------
#--- DAEMON ---
#--------------
 
unless ($nofork) {
    my $d = PGagent::Daemon->new;
    my $user = $app->config('user');
    my $group = $app->config('group');
    $d->fork;
    $app->log(Mojo::Log->new( 
                path => $app->config('logfile'),
                level => $app->config('loglevel')
    ));
}
 
$server->pid_file($app->config('pidfile'));
 
#---------------
#--- WEB LOG ---
#---------------
 
$app->hook(before_dispatch => sub {
        my $c = shift;
 
        my $remote_address = $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;
        my $url = $c->req->url->to_abs->to_string;
 
        unless ($loglevel eq 'debug') {
            #$c->app->log->info("$remote_address $method $base$path");
            $c->app->log->info("$remote_address $method $url");
        }
        if ($loglevel eq 'debug') {
            $c->app->log->debug("$remote_address $method $url");
        }
});
 
#----------------------
#--- SIGNAL HANDLER ---
#----------------------
 
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;
#EOF

pgmaster.pl

pgmaster.pl
#!@PERL@
 
#--------------
#--- DAEMON ---
#--------------
 
package PGmaster::Daemon;
 
use strict;
use warnings;
use POSIX qw(getpid setuid setgid geteuid getegid);
use Cwd qw(cwd getcwd chdir);
use Mojo::Util qw(dumper);
 
sub new {
    my $class = shift;
    my $self = {
        pid => undef
    };
    bless $self, $class;
    return $self;
}
 
sub fork {
    my ($self, $user, $group) = shift;
    my $pid = fork;
    if ($pid > 0) {
        exit;
    }
    chdir("/");
    open(my $stdout, '>&', STDOUT); 
    open(my $stderr, '>&', STDERR);
    open(STDOUT, '>>', '/dev/null');
    open(STDERR, '>>', '/dev/null');
    $self->pid(getpid);
    $self->pid;
}
 
sub pid {
    my ($self, $pid) = @_;
    return $self->{pid} unless $pid;
    $self->{pid} = $pid if $pid;
    $self;
}
 
1;
 
#----------
#--- DB ---
#----------
 
package PGmaster::DB;
 
use strict;
use warnings;
use DBI;
 
sub new {
    my ($class, %args) = @_;
    my $self = {
        hostname => $args{hostname},
        username => $args{username},
        password => $args{password},
        database => $args{database},
        engine => 'Pg',
        error => ''
    };
    bless $self, $class;
    return $self;
}
 
sub username {
    my ($self, $username) = @_; 
    return $self->{username} unless $username;
    $self->{username} = $username;
    $self;
}
 
sub password {
    my ($self, $password) = @_; 
    return $self->{password} unless $password;
    $self->{password} = $password;
    $self;
}
 
sub hostname {
    my ($self, $hostname) = @_; 
    return $self->{hostname} unless $hostname;
    $self->{hostname} = $hostname;
    $self;
}
 
sub database {
    my ($self, $database) = @_; 
    return $self->{database} unless $database;
    $self->{database} = $database;
    $self;
}
 
sub error {
    my ($self, $error) = @_; 
    return $self->{error} unless $error;
    $self->{error} = $error;
    $self;
}
 
sub engine {
    my ($self, $engine) = @_; 
    return $self->{engine} unless $engine;
    $self->{engine} = $engine;
    $self;
}
 
 
sub exec {
    my ($self, $query) = @_;
    return undef unless $query;
 
    my $dsn = 'dbi:'.$self->engine.
                ':dbname='.$self->database.
                ';host='.$self->hostname;
    my $dbi;
    eval {
        $dbi = DBI->connect($dsn, $self->username, $self->password, { 
            RaiseError => 1,
            PrintError => 0,
            AutoCommit => 1 
        });
    };
    $self->error($@);
    return undef if $@;
 
    my $sth;
    eval {
        $sth = $dbi->prepare($query);
    };
    $self->error($@);
    return undef if $@;
 
    my $rows = $sth->execute;
    my @list;
 
    while (my $row = $sth->fetchrow_hashref) {
        push @list, $row;
    }
    $sth->finish;
    $dbi->disconnect;
    \@list;
}
 
sub exec1 {
    my ($self, $query) = @_;
    return undef unless $query;
 
    my $dsn = 'dbi:'.$self->engine.
                ':dbname='.$self->database.
                ';host='.$self->hostname;
    my $dbi;
    eval {
        $dbi = DBI->connect($dsn, $self->username, $self->password, { 
            RaiseError => 1,
            PrintError => 0,
            AutoCommit => 1 
        });
    };
    $self->error($@);
    return undef if $@;
 
    my $sth;
    eval {
        $sth = $dbi->prepare($query);
    };
    $self->error($@);
    return undef if $@;
 
    my $rows = $sth->execute;
    my $row = $sth->fetchrow_hashref;
 
    $sth->finish;
    $dbi->disconnect;
    $row;
}
 
sub do {
    my ($self, $query) = @_;
    return undef unless $query;
    my $dsn = 'dbi:'.$self->engine.
                ':dbname='.$self->database.
                ';host='.$self->hostname;
    my $dbi;
    eval {
        $dbi = DBI->connect($dsn, $self->username, $self->password, { 
            RaiseError => 1,
            PrintError => 0,
            AutoCommit => 1 
        });
    };
    $self->error($@);
    return undef if $@;
    my $rows;
    eval {
        $rows = $dbi->do($query) or return undef;
    };
    $self->error($@);
    return undef if $@;
 
    $dbi->disconnect;
    $rows*1;
}
 
1;
 
#-------------
#--- MODEL ---
#-------------
 
package PGmaster::Model;
 
use strict;
use warnings;
use File::stat;
use DBI;
use Mojo::UserAgent;
use Mojo::JSON qw(encode_json decode_json);
use Mojo::Util qw(md5_sum dumper);
use Socket;
use POSIX;
 
sub new {
    my ($class, $app, $db) = @_;
    my $self = {
        app => $app,
        db => $db
    };
    bless $self, $class;
    return $self;
}
 
sub db {
    return shift->{db};
}
 
sub app {
    return shift->{app};
}
 
sub hostname_alive {
    my ($self, $hostname) = @_;
    return undef unless gethostbyname($hostname);
    return 1;
}
 
sub parse_label {
    my ($self, $label) = @_;
    return undef unless $label;
 
    $label =~ s/.sqlz$//;
 
    my ($dbname, $Y, $M, $D, $h, $m, $s, $tz, $host) =
        $label =~ /([_a-z0-9]{1,64})--([0-9]{4})([0-9]{2})([0-9]{2})-([0-9]{2})([0-9]{2})([0-9]{2})-([A-Z0-9\+]{2,4})--([_\-a-zA-Z0-9\.]{1,64})/g;
 
    my %data;
    $data{'dbname'} = $dbname;
    $data{'timestamp'} = "$Y-$M-$D $h:$m:$s $tz";
    $data{'datetime'} = "$Y-$M-$D $h:$m:$s";
    $data{'tz'} = "$tz";
    $data{'source'} = $host;
    return \%data;
}
 
sub timestamp {
    my ($self, $timenum) = shift;
    $timenum = time unless $timenum;
    return strftime("%Y-%m-%d %H:%M:%S %Z", localtime($timenum));
}
 
sub curr_sec {
    my $self = shift;
    return strftime("%S", localtime(time));
}
 
sub curr_time {
    my $self = shift;
    my $time = time;
    my %time;
    $time{sec} = strftime("%S", localtime($time));
    $time{min} = strftime("%M", localtime($time));
    $time{hour} = strftime("%H", localtime($time));
    $time{mday} = strftime("%d", localtime($time));
    $time{month} = strftime("%m", localtime($time));
    $time{yarth} = strftime("%Y", localtime($time));
    $time{wday} = strftime("%u", localtime($time));
    return \%time;
}
 
sub size_hr {
    my ($self, $size) = @_;
    return $size if $size < 1024;
    return int($size/1024+0.5)."k" if ($size < 1024*1024 && $size > 1024);
    return int($size/(1024*1024)+0.5)."M" if ($size < 1024*1024*1024 && $size > 1024*1024);
    return int($size/(1024*1024*1024)+0.5)."G" if ($size < 1024*1024*1024*1024 && $size > 1024*1024*1024);
}
 
sub size_wp {
    my ($self, $size) = @_;
    my $out = $size;
    $out =~ s/(\d{9})$/.$1/g if $size > 1000*1000*1000;
    $out =~ s/(\d{6})$/.$1/g if $size > 1000*1000;
    $out =~ s/(\d{3})$/.$1/g if $size > 1000;
    return $out;
}
 
sub size_m {
    my ($self, $size) = @_;
    return int($size/(1024*1024)+0.5);
}
 
sub strip_sec {
    my ($self, $s) = @_;
    my $a = substr $s, 0, 16;
    $a =~ s/\//-/g;
    return $a;
}
 
#-------------------
#--- AGENT MODEL ---
#-------------------
 
sub agent_list {
    my ($self, $id)  = @_;
    my $where = '';
    $where = "where id = $id" if $id;
    my $query = "select * from agent $where order by hostname";
    $self->db->exec($query);
}
 
sub agent_exist {
    my ($self, $hostname)  = @_;
    return undef unless $hostname;
    my $query = "select id from agent where hostname = '$hostname' limit 1";
    my $row = $self->db->exec1($query);
    my $id = $row->{'id'};
    return undef unless $id;
    $id;
}
 
sub agent_hostname {
    my ($self, $id)  = @_;
    return undef unless $id;
    my $query = "select hostname from agent where id = $id limit 1";
    my $row = $self->db->exec1($query);
    my $hostname = $row->{'hostname'};
    return undef unless $hostname;
    $hostname;
}
 
sub agent_profile {
    my ($self, $id)  = @_;
    return undef unless $id;
 
    my $query = "select * from agent where id = $id limit 1";
    my $row = $self->db->exec1($query);
    return undef unless $row;
    $row;
}
 
sub agent_info {
    my ($self, $id)  = @_;
    return undef unless $id;
    my $query = "select sum(d.size) as sum , count(d.name) as count
                    from agent a, db d
                    where a.id = d.agentid and a.id = $id limit 1";
    my $row = $self->db->exec1($query);
    $row;
}
 
sub agent_next_id {
    my $self = shift;
    my $query = "select id from agent order by id desc limit 1";
    my $row = $self->db->exec1($query);
    my $id = $row->{'id'} || 0;
    $id += 1;
}
 
sub agent_add {
    my ($self, $hostname, $username, $password) = @_;
    return undef unless $hostname;
    return undef unless $username;
    return undef unless $password;
 
    my $id = $self->agent_next_id;
    my $query = "insert into agent (id, hostname, username, password)
                    values ($id, '$hostname', '$username', '$password')";
    $self->db->do($query);
    $self->agent_exist($hostname);
}
 
sub agent_delete {
    my ($self, $id) = @_;
    return undef unless $id;
 
    my $query = "delete from agent where id = $id";
    $self->db->do($query);
}
 
sub agent_config {
    my ($self, $id, $hostname, $username, $password) = @_;
    my $query = "update agent set
                    hostname = '$hostname',
                    username = '$username',
                    password = '$password'
                    where id = $id";
    $self->db->do($query);
}
 
sub agent_alive {
    my ($self, $id) = @_;
    return undef unless $id;
    my $hostname = $self->agent_hostname($id);
    return undef unless $hostname;
    my $ua = Mojo::UserAgent->new;
    $ua = $ua->connect_timeout(5);
 
 
    my $tx = $ua->get("https://$hostname:3001/hello");
    my $body;
    eval {
        $body = $tx->result->body;
    };
    do { $self->app->log->info("agent_alive: $@");  return undef; } if $@;
    return undef unless $body;
    my $res = decode_json $body;
    return 1 if $res->{'message'} eq 'hello';
    return undef;
}
 
sub agent_db_list {
    my ($self, $id) = @_;
    return undef unless $id;
    my $query = "select * from db where agentid = $id order by name";
    $self->db->exec($query);
}
 
sub agent_db_update {
    my ($self, $id) = @_;
    return undef unless $id;
    my $agent_profile = $self->agent_profile($id) or return undef;
 
    my $hostname = $agent_profile->{'hostname'} || 'undef';
    my $username = $agent_profile->{'username'} || 'undef';
    my $password = $agent_profile->{'password'} || 'password';
 
    my $ua = Mojo::UserAgent->new(max_redirects => 2) or return undef;
    $ua = $ua->connect_timeout(5);
    my $tx = $ua->get("https://$username:$password\@$hostname:3001/db/list");
 
    my $res;
    eval { $res = decode_json($tx->result->body); };
    return undef if $@;
 
    my $dblist = $res->{'dblist'};
    return undef unless $dblist;
 
    my $query = "delete from db where agentid = $id";
    $self->db->do($query);
 
    foreach my $rec (@{$dblist}) {
        my $dbname = $rec->{'name'} || 'undef';
        my $dbsize = $rec->{'size'} || 1;
        my $dbowner = $rec->{'owner'} || 'undef';
        my $numbackends = $rec->{'numbackends'} || 0;
        my $query = "insert into db (agentid, name, size, owner, type, numbackends) values
                    ($id, '$dbname',  $dbsize, '$dbowner', 'pgsql', $numbackends)";
        my $rows = $self->db->do($query);
    }
    $dblist;
}
 
#-------------------
#--- STORE MODEL ---
#-------------------
 
sub store_hostname {
    my ($self, $id)  = @_;
    return undef unless $id;
    my $query = "select hostname from store where id = $id limit 1";
    my $row = $self->db->exec1($query);
    my $hostname = $row->{'hostname'};
    return undef unless $hostname;
    $hostname;
}
 
sub store_profile {
    my ($self, $id)  = @_;
    return undef unless $id;
    my $query = "select * from store where id = $id limit 1";
    $self->db->exec1($query);
}
 
sub store_info {
    my ($self, $id)  = @_;
    return undef unless $id;
    my $query = "select sum(d.size) as sum , count(d.name) as count
                    from store s, data d
                    where s.id = d.storeid and s.id = $id limit 1";
    my $row = $self->db->exec1($query);
    return undef unless $row;
    $row->{'sum'} ||= 0;
    $row;
}
 
sub store_alive {
    my ($self, $id) = @_;
    return undef unless $id;
    my $hostname = $self->store_hostname($id);
    return undef unless $hostname;
 
    my $ua = Mojo::UserAgent->new;
    $ua = $ua->connect_timeout(5);
    my $tx = $ua->get("https://$hostname:3002/hello");
 
    my $body;
    eval { $body = $tx->result->body; };
    do { $self->app->log->info("store_alive: $@"); return undef; } if $@;
    return undef unless $body;
 
    my $res = decode_json $body;
 
    return 1 if $res->{'message'} eq 'hello';
    return undef;
}
 
sub store_list {
    my ($self, $id)  = @_;
    my $where = '';
    $where = "where id = '$id'" if $id;
    my $query = "select * from store $where order by hostname";
    $self->db->exec($query);
}
 
sub store_exist {
    my ($self, $hostname)  = @_;
    return undef unless $hostname;
 
    my $query = "select id from store where hostname = '$hostname' limit 1";
    my $row = $self->db->exec1($query);
    my $id = $row->{'id'};
    return undef unless $id;
    $id;
}
 
sub store_next_id {
    my $self = shift;
    my $query = "select id from store order by id desc limit 1";
    my $row = $self->db->exec1($query);
    my $id = $row->{'id'} || 0;
    $id += 1;
}
 
sub store_add {
    my ($self, $hostname, $username, $password) = @_;
    return undef unless $hostname;
    return undef unless $username;
    return undef unless $password;
    my $id = $self->store_next_id;
    my $query = "insert into store (id, hostname, username, password)
                    values ($id, '$hostname', '$username', '$password')";
    $self->db->do($query);
    $self->store_exist($hostname);
}
 
sub store_delete {
    my ($self, $id) = @_;
    return undef unless $id;
    my $query = "delete from store where id = $id";
    $self->db->do($query);
}
 
sub store_config {
    my ($self, $id, $hostname, $username, $password) = @_;
 
    my $query = "update store set
                    hostname = '$hostname',
                    username = '$username',
                    password = '$password'
                    where id = $id";
    $self->db->do($query);
}
 
sub store_data_list {
    my ($self, $id) = @_;
    return undef unless $id;
    my $query = "select * from data where storeid = $id order by name";
    $self->db->exec($query);
}
 
sub store_data_update {
    my ($self, $id) = @_;
    return undef unless $id;
    my $store_profile = $self->store_profile($id) or return undef;
 
    my $hostname = $store_profile->{'hostname'} || 'undef';
    my $username = $store_profile->{'username'} || 'undef';
    my $password = $store_profile->{'password'} || 'password';
 
    my $ua = Mojo::UserAgent->new(max_redirects => 2) or return undef;
    $ua = $ua->connect_timeout(5);
    my $tx = $ua->get("https://$username:$password\@$hostname:3002/data/list");
 
    my $res = undef;
    eval { $res = decode_json($tx->result->body); };
    return undef if $@;
 
    my $free = $res->{'free'} || 0;
    my $datalist = $res->{'datalist'} || undef;
 
    return undef unless $datalist;
 
    my $query = "delete from data where storeid = $id";
    $self->db->do($query);
 
    $query = "update store set free = $free where id = $id";
    $self->db->do($query);
 
    my @list;
    foreach my $rec (@{$datalist}) {
        my $dataname = $rec->{'name'};
        my $datasize = $rec->{'size'};
        my $datamtime = $rec->{'mtime'};
 
        # Dataname pattern for dumps dbname--timestamp--sourcehost.ext
        next unless $dataname =~ m/.+--.+--.+\.(sqlz|sql|sql.gz|sql.xz)/;
 
        my $label = $self->app->model->parse_label($dataname);
        my $dbname = $label->{'dbname'} || 'undef';
        my $source = $label->{'source'} || 'undef';
        my $stamp = $label->{'timestamp'} || '1970-01-01 00:00:00 UTC';
        my $datetime = $label->{'datetime'} || '1970-01-01 00:00:00';
        my $tz = $label->{'tz'} || 'UND';
 
        my $query = "insert into data (storeid, name, size, mtime, type, dbname, source, stamp, datetime, tz) values
                    ($id, '$dataname',  $datasize, '$datamtime', 'pgsql', '$dbname', '$source', '$stamp', '$datetime', '$tz')";
        $self->db->do($query);
        push @list, $rec;
    }
    return \@list;
}
 
sub store_free {
    my ($self, $id)  = @_;
    return undef unless $id;
    my $query = "select free from store where id = $id limit 1";
    my $row = $self->db->exec1($query);
    my $free = $row->{'free'};
    return undef unless $free;
    $free;
}
 
#---------------
#--- XXXXXXX ---
#---------------
 
sub data_list {
    my $self = shift;
    my $dbi = DBI->connect($self->dsn, $self->dbuser, $self->dbpasswd)
        or return undef;
    my $query = "select s.hostname as store, d.* from data d, store s where s.id = d.storeid order by d.dbname";
    my $sth = $dbi->prepare($query);
    my $rows = $sth->execute;
    my @list;
    while (my $row = $sth->fetchrow_hashref) {
        push @list, $row;
    }
    $sth->finish;
    $dbi->disconnect;
    return \@list;
}
 
#-----------------
#--- JOB MODEL ---
#-----------------
 
sub job_create {
    my ($self, @args) = @_;
 
    my $id = $self->job_next_id;
 
    my $begin = strftime("%Y-%m-%d %H:%M:%S %Z", localtime(time));
    my $stop = '1970-01-01 00:00:00 UTC';
    my $type = "undef";
    my $author = "undef";
    my $sourceid = 0;
    my $destid = 0;
    my $status = "undef";
    my $error = "undef";
    my $message = "Job record created";
    my $magic = md5_sum(localtime(time));
 
    my $query = "insert into job (id, begin, stop, author, type, sourceid, destid, status, error, message, magic)
                    values ($id, '$begin', '$stop', '$author', '$type', $sourceid, $destid, '$status', '$error', '$message', '$magic')";
    $self->db->do($query);
    $id;
}
 
sub job_list {
    my $self = shift;
    my $query = "select id,
                    to_char(begin, 'YYYY-MM-DD HH24:MI:SS TZ') as begin,
                    to_char(stop, 'YYYY-MM-DD HH24:MI:SS TZ') as stop,
                    author, type, sourceid, destid, status, error, message, magic from job
                        order by id desc limit 100";
    $self->db->exec($query);
}
 
sub job_next_id {
    my $self = shift;
    my $query = "select id from job order by id desc limit 1";
    my $row = $self->db->exec1($query);
    my $id = $row->{'id'} || 0;
    $id += 1;
}
 
sub job_exist {
    my ($self, $id)  = @_;
    my $query = "select id from job where id = $id limit 1";
    my $row = $self->db->exec1($query);
    $id = $row->{'id'};
    return undef unless $id;
    $id;
}
 
sub job_update {
    my ($self, $id, %args) = @_;
 
    return undef unless $id;
    return undef unless $self->job_exist($id);
 
    my $job_profile = $self->job_profile($id);
    my $args = \%args;
 
    my $begin = $args->{'begin'} || $job_profile->{'begin'};
    my $stop = $args->{'stop'} || $job_profile->{'stop'};
    my $type = $args->{'type'} || $job_profile->{'type'};
    my $author = $args->{'author'} || $job_profile->{'author'};
    my $sourceid = $args->{'sourceid'} || $job_profile->{'sourceid'};
    my $destid = $args->{'destid'} || $job_profile->{'destid'};
 
    my $status = $args->{'status'} || $job_profile->{'status'};
    my $error = $args->{'error'} || $job_profile->{'error'};
    my $message = $args->{'message'} || $job_profile->{'message'};
 
    my $query = "update job set
                    begin = '$begin',
                    stop = '$stop',
                    author = '$author',
                    type = '$type',
                    sourceid = $sourceid,
                    destid = $destid,
                    status = '$status',
                    error = '$error',
                    message = '$message'
                        where id = $id";
    my $rows = $self->db->do($query);
    $id;
}
 
sub job_delete {
    my ($self, $id) = @_;
    my $query = "delete from job where id = $id";
    $self->db->do($query);
    $id;
}
 
sub job_profile {
    my ($self, $id)  = @_;
    my $query = "select id, to_char(begin, 'YYYY-MM-DD HH24:MI:SS TZ') as begin,
                    to_char(stop, 'YYYY-MM-DD HH24:MI:SS TZ') as stop,
                    author, type, sourceid, destid, status, error, message, magic from job where id = $id limit 1";
 
    my $row = $self->db->exec1($query);
    return undef unless $row;
    $row;
}
 
sub job_cb {
    my ($self, $req)  = @_;
 
    my $job_id = $req->param('jobid');
    my $magic = $req->param('magic');
 
    return undef unless $job_id;
    return undef unless $magic;
 
    my $job_profile = $self->job_profile($job_id);
 
    my $error = $req->param('error');
    my $status = $req->param('status');
    my $message = $req->param('message');
 
    $self->job_update($job_id, error => $error, stop => $self->timestamp) if $error;
    $self->job_update($job_id, status => $status, stop => $self->timestamp) if $status;
    $self->job_update($job_id, message => $message, stop => $self->timestamp) if $message;
    1;
}
#----------------------
#--- SCHEDULE MODEL ---
#----------------------
 
sub schedule_list {
    my ($self, $id)  = @_;
    my $where = '';
    $where = "where id = $id" if $id;
    my $query = "select * from schedule $where order by id";
    $self->db->exec($query);
}
 
sub schedule_profile {
    my ($self, $id)  = @_;
    return undef unless $id;
    my $query = "select * from schedule where id = $id limit 1";
    $self->db->exec1($query);
}
 
sub schedule_next_id {
    my $self = shift;
    my $query = "select id from schedule order by id desc limit 1";
    my $row = $self->db->exec1($query);
    my $id = $row->{'id'};
    $id++;
    $id;
}
 
sub schedule_add {
    my ($self, $type, $source_id, $dest_id, $subject, $mday, $wday, $hour, $min) = @_;
 
    return undef unless $type;
    return undef unless $source_id;
    return undef unless $dest_id;
    return undef unless $subject;
    return undef unless $mday;
    return undef unless $wday;
    return undef unless $hour;
    return undef unless $min;
 
    my $id = $self->schedule_next_id;
    return undef unless $id;
 
    my $dbi = DBI->connect($self->dsn, $self->dbuser, $self->dbpasswd)
         or return undef;
    my $query = "insert into schedule (id, type, sourceid, destid, subject, mday, wday, hour, min)
                    values ($id, '$type', $source_id, $dest_id, '$subject', '$mday', '$wday', '$hour', '$min')";
    my $rows = $dbi->do($query) or return undef;
    $dbi->disconnect;
 
    return $id if $rows*1 > 0;
    return undef;
}
 
sub period_expand {
    my ($self, $def, $limit, $start) = @_;
    $limit = 100 unless defined $limit;
    $start = 1 unless defined $start;
    my @list = split ',', $def;
    my @out;
    my %n;
    foreach my $sub (@list) {
        if (my ($num) = $sub =~ m/^(\d+)$/ ) {
                next if $num < $start;
                next if $num > $limit;
                push @out, $num unless $n{$num};
                $n{$num} = 1;
        }
        elsif (my ($begin, $end) = $sub =~ /^(\d+)[-. ]+(\d+)$/) {
            foreach my $num ($begin..$end) {
                next if $num < $start;
                next if $num > $limit;
                push @out, $num unless $n{$num};
                $n{$num} = 1;
            }
        }
        elsif (my ($inc) = $sub =~ /^[*]\/(\d+)$/) {
            my $num = $start;
            while ($num <= $limit) {
                next if $num < $start;
                push @out, $num unless $n{$num};
                $num += $inc;
            }
        }
        elsif ($sub =~ /^[*]$/) {
            my $num = $start;
            my $inc = 1;
            while ($num <= $limit) {
                next if $num < $start;
                next if $num > $limit;
                push @out, $num unless $n{$num};
                $num += $inc;
            }
        }
    }
    @out = sort { $a <=> $b } @out;
    return \@out
}
 
sub period_compact {
    my ($self, $list) = @_;
    my %n;
    my $out;
    foreach my $num (@{$list}) { $n{$num} = 1; }
    foreach my $num (sort { $a <=> $b } (keys %n)) {
        $out .= $num."-" if $n{$num+1} && not $n{$num-1};
        $out .= $num."," unless $n{$num+1};
    }
    $out =~ s/,$//;
    return $out;
}
 
sub period_match {
    my ($self, $num, $list) = @_;
    foreach my $elem (@{$list}) {
        return 1 if $num == $elem;
    }
    return undef;
}
 
#-----------------
#--- MODEL END ---
#-----------------
1;
 
#==================
#------------------
#--- CONTROLLER ---
#------------------
#==================
 
package PGmaster::Controller;
 
use utf8;
use strict;
use warnings;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Util qw(md5_sum dumper);
use Mojo::JSON qw(encode_json decode_json);
use Apache::Htpasswd;
 
sub hello {
    my $self = shift;
    $self->render(template => 'hello');
}
 
#------------------
#--- AGENT CONT ---
#------------------
 
sub agent_list {
    my $self = shift;
    $self->render(template => 'agent_list');
}
 
sub agent_add {
    my $self = shift;
    $self->render(template => 'agent_add', req => $self->req);
}
 
sub agent_config {
    my $self = shift;
    $self->render(template => 'agent_config', req => $self->req);
}
 
sub agent_delete {
    my $self = shift;
    $self->render(template => 'agent_delete', req => $self->req);
}
 
sub agent_db_list {
    my $self = shift;
    my $id = $self->req->param('id');
    $self->render(template => 'agent_db_list', req => $self->req);
}
 
sub agent_db_create {
    my $self = shift;
    $self->render(template => 'agent_db_create', req => $self->req);
}
 
sub agent_db_drop {
    my $self = shift;
    $self->render(template => 'agent_db_drop', req => $self->req);
}
 
sub agent_db_rename {
    my $self = shift;
    $self->render(template => 'agent_db_rename', req => $self->req);
}
 
sub agent_db_copy {
    my $self = shift;
    $self->render(template => 'agent_db_copy', req => $self->req);
}
 
sub agent_db_dump {
    my $self = shift;
    $self->render(template => 'agent_db_dump', req => $self->req);
}
 
sub agent_db_restore {
    my $self = shift;
    $self->render(template => 'agent_db_restore', req => $self->req);
}
 
#------------------
#--- STORE CONT ---
#------------------
 
sub store_list {
    my $self = shift;
    $self->render(template => 'store_list');
}
 
sub store_add {
    my $self = shift;
    $self->render(template => 'store_add', req => $self->req);
}
 
sub store_config {
    my $self = shift;
    $self->render(template => 'store_config', req => $self->req);
}
 
sub store_delete {
    my $self = shift;
    $self->render(template => 'store_delete', req => $self->req);
}
 
sub store_data_list {
    my $self = shift;
    $self->render(template => 'store_data_list', req => $self->req);
}
 
sub store_data_delete {
    my $self = shift;
    $self->render(template => 'store_data_delete', req => $self->req);
}
 
sub store_data_download {
    my $self = shift;
    $self->render(template => 'store_data_download', req => $self->req);
}
 
 
#-------------
#--- XXXXX ---
#-------------
 
sub data_list {
    my $self = shift;
    $self->render(template => 'data_list', req => $self->req);
}
 
#----------------
#--- JOB CONT ---
#----------------
 
sub job_list {
    my $self = shift;
    $self->render(template => 'job_list', req => $self->req);
}
 
sub job_cb {
    my $self = shift;
    $self->app->model->job_cb($self->req);
    $self->render(json => { responce => 'success' } );
}
 
#---------------------
#--- SCHEDULE CONT ---
#---------------------
 
sub schedule_list {
    my $self = shift;
    $self->render(template => 'schedule_list', req => $self->req);
}
 
sub schedule_add {
    my $self = shift;
    $self->render(template => 'schedule_add', req => $self->req);
}
 
sub schedule_delete {
    my $self = shift;
    $self->render(template => 'schedule_delete', req => $self->req);
}
 
sub schedule_config {
    my $self = shift;
    $self->render(template => 'schedule_config', req => $self->req);
}
 
#--------------------
#--- SESSION CONT ---
#--------------------
 
sub pwfile {
    my ($self, $pwdfile) = @_;
    return $self->app->config('pwdfile') unless $pwdfile;
    $self->app->config(pwfile => $pwdfile);
}
 
sub ucheck {
    my ($self, $username, $password) = @_;
    return undef unless $password;
    return undef unless $username;
    my $pwdfile = $self->pwfile or return undef;
    my $res = undef;
    eval {
        my $ht = Apache::Htpasswd->new({ passwdFile => $pwdfile, ReadOnly => 1 });
        $res = $ht->htCheckPassword($username, $password);
    };
    $res;
}
 
sub login {
    my $self = shift;
    return $self->redirect_to('/') if $self->session('username');
 
    my $username = $self->req->param('username') || undef;
    my $password = $self->req->param('password') || undef;
 
    return $self->render(template => 'login') unless $username and $password;
 
    if ($self->ucheck($username, $password)) {
        $self->session(username => $username);
        return $self->redirect_to('/');
    }
    $self->render(template => 'login');
}
 
sub logout {
    my $self = shift;
    $self->session(expires => 1);
    $self->redirect_to('/');
}
 
 
#----------------
#--- CONT END ---
#----------------
1;
 
#-----------
#--- APP ---
#-----------
package PGmaster;
 
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 tzset tzname strftime);
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 $appname = 'pgmaster';
 
#--------------
#--- GETOPT ---
#--------------
 
getopt
    'h|help' => \my $help,
    'c|config=s' => \my $conffile,
    '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
    -c | --config=path    Path to config file
    -u | --user=user      System owner of process
    -g | --group=group    System group 
    -f | --nofork         Dont fork process
 
The options override options from configuration file
    )."\n";
    exit 0;
}
 
#------------------
#--- APP CONFIG ---
#------------------
 
my $server = Mojo::Server::Prefork->new;
my $app = $server->build_app('PGmaster');
$app = $app->controller_class('PGmaster::Controller');
 
$app->secrets(['6d578e43ba88260e0375a1a35fd7954b']);
$app->static->paths(['@APP_LIBDIR@/public']);
$app->renderer->paths(['@APP_LIBDIR@/templs']);
 
$app->config(conffile => $conffile || '@APP_CONFDIR@/pgmaster.conf');
$app->config(pwdfile => '@APP_CONFDIR@/pgmaster.pw');
$app->config(logfile => '@APP_LOGDIR@/pgmaster.log');
$app->config(loglevel => 'info');
$app->config(pidfile => '@APP_RUNDIR@/pgmaster.pid');
$app->config(crtfile => '@APP_CONFDIR@/pgmaster.crt');
$app->config(keyfile => '@APP_CONFDIR@/pgmaster.key');
 
$app->config(user => $user || '@APP_USER@');
$app->config(group => $group || '@APP_GROUP@');
 
$app->config(listenaddr4 => '0.0.0.0');
$app->config(listenaddr6 => '[::]');
$app->config(listenport => '3003');
 
$app->config(tmpdir => '/tmp');
 
$app->config(dbhost => '127.0.0.1');
$app->config(dbuser => 'postgres');
$app->config(dbpwd => 'password');
$app->config(dbname => 'pgdumper');
 
if (-r $app->config('conffile')) {
    $app->log->debug("Load configuration from ".$app->config('conffile'));
    $app->plugin('JSONConfig', { file => $app->config('conffile') });
}
 
#---------------
#--- HELPERS ---
#---------------
 
$app->helper('reply.not_found' => sub {
        my $c = shift; 
        return $c->redirect_to('/login') unless $c->session('username'); 
        $c->render(template => 'not_found.production');
});
 
$app->helper(
    model => sub {
        my $db = PGmaster::DB->new(
                        hostname => $app->config("dbhost"),
                        username => $app->config("dbuser"),
                        password => $app->config("dbpwd"),
                        database => $app->config("dbname"),
        );
        state $model = PGmaster::Model->new($app, $db);
    }
);
 
#--------------
#--- ROUTES ---
#--------------
 
my $r = $app->routes;
 
$r->add_condition(
    auth => sub {
        my ($route, $c) = @_;
        $c->session('username');
    }
);
 
$r->any('/login')->to('controller#login');
$r->any('/logout')->to('controller#logout');
$r->any('/hello')->over('auth')->to('controller#hello');
# agent forms
$r->any('/agent/list')->over('auth')->to('controller#agent_list');
# agent handlers
$r->any('/agent/add')->over('auth')->to('controller#agent_add');
$r->any('/agent/config')->over('auth')->to('controller#agent_config');
$r->any('/agent/delete')->over('auth')->to('controller#agent_delete');
# agent db forms
$r->any('/agent/db/list')->over('auth')->to('controller#agent_db_list');
# agent db handlers
$r->any('/agent/db/create')->over('auth')->to('controller#agent_db_create');
$r->any('/agent/db/drop')->over('auth')->to('controller#agent_db_drop');
$r->any('/agent/db/copy')->over('auth')->to('controller#agent_db_copy');
$r->any('/agent/db/rename')->over('auth')->to('controller#agent_db_rename');
$r->any('/agent/db/dump')->over('auth')->to('controller#agent_db_dump');
$r->any('/agent/db/restore')->over('auth')->to('controller#agent_db_restore');
 
# store forms
$r->any('/store/list')->over('auth')->to('controller#store_list');
# store handlers
$r->any('/store/add')->over('auth')->to('controller#store_add');
$r->any('/store/config')->over('auth')->to('controller#store_config');
$r->any('/store/delete')->over('auth')->to('controller#store_delete');
# store data manipulation
$r->any('/store/data/list')->over('auth')->to('controller#store_data_list');
$r->any('/store/data/delete')->over('auth')->to('controller#store_data_delete');
$r->any('/store/data/download')->over('auth')->to('controller#store_data_download');
 
# generic data list
$r->any('/data/list')->over('auth')->to('controller#data_list');
# generic job list
$r->any('/job/list')->over('auth')->to('controller#job_list');
 
 
$r->any('/schedule/list')->over('auth')->to('controller#schedule_list');
$r->any('/schedule/add')->over('auth')->to('controller#schedule_add');
$r->any('/schedule/delete')->over('auth')->to('controller#schedule_delete');
$r->any('/schedule/config')->over('auth')->to('controller#schedule_config');
 
# callback handlers
$r->any('/agent/job/cb')->to('controller#job_cb');
$r->any('/store/job/cb')->to('controller#job_cb');
 
#----------------
#--- LISTENER ---
#----------------
 
my $tls = '?';
$tls .= 'cert='.$app->config('crtfile');
$tls .= '&key='.$app->config('keyfile');
 
my $listen4;
if ($app->config('listenaddr4')) {
    $listen4 = "https://";
    $listen4 .= $app->config('listenaddr4').':'.$app->config('listenport');
    $listen4 .= $tls;
}
 
my $listen6;
if ($app->config('listenaddr6')) {
    $listen6 = "https://";
    $listen6 .= $app->config('listenaddr6').':'.$app->config('listenport');
    $listen6 .= $tls;
}
 
my @listen;
push @listen, $listen4 if $listen4;
push @listen, $listen6 if $listen6;
 
$server->listen(\@listen);
$server->heartbeat_interval(3);
$server->heartbeat_timeout(60);
 
#--------------
#--- DAEMON ---
#--------------
 
unless ($nofork) {
    my $d = PGmaster::Daemon->new;
    my $user = $app->config('user');
    my $group = $app->config('group');
    $d->fork;
    $app->log(Mojo::Log->new( 
                path => $app->config('logfile'),
                level => $app->config('loglevel')
    ));
}
 
$server->pid_file($app->config('pidfile'));
 
#---------------
#--- WEB LOG ---
#---------------
 
$app->hook(before_dispatch => sub {
        my $c = shift;
 
        my $remote_address = $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;
        my $url = $c->req->url->to_abs->to_string;
 
        unless ($loglevel eq 'debug') {
            #$c->app->log->info("$remote_address $method $base$path");
            $c->app->log->info("$remote_address $method $url");
        }
        if ($loglevel eq 'debug') {
            $c->app->log->debug("$remote_address $method $url");
        }
});
 
#----------------------
#--- SIGNAL HANDLER ---
#----------------------
 
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;
 
 
#EOF

pgstore.pl

pgstore.pl
#!@PERL@
 
1;
 
#------------
#--- AUTH ---
#------------
 
package PGstore::BasicAuth;
 
use strict;
use warnings;
use POSIX qw(getpid setuid setgid geteuid getegid);
use Cwd qw(cwd getcwd chdir);
use Mojo::Util qw(md5_sum b64_decode dumper);
use Apache::Htpasswd;
 
sub new {
    my ($class, $pwdfile) = @_;
    my $self = {
        pwdfile => $pwdfile,
        errstr => undef
    };
    bless $self, $class;
    return $self;
}
 
sub pwdfile {
    my ($self, $pwdfile) = @_;
    return $self->{pwdfile} unless $pwdfile;
    $self->{pwdfile} = $pwdfile if $pwdfile;
    $self;
}
 
sub auth {
    my ($self, $authstr) = @_;
    return undef unless $authstr;
 
    my $hash = $self->split($authstr);
    return undef unless $hash;
    return undef unless -r $self->{pwdfile};
 
    my $res = undef;
    eval {
        my $ht = Apache::Htpasswd->new( { passwdFile => $self->pwdfile, ReadOnly => 1 } );
        $res = $ht->htCheckPassword(
                            $hash->{username},
                            $hash->{password}
        );
    };
    return undef if $@;
    $res;
}
 
sub username {
    my ($self, $authstr) = @_;
    return undef unless $authstr;
    my $hash = $self->split($authstr);
    return undef unless $hash;
    $hash->{username} if $hash;
}
 
sub split {
    my ($self, $authstr) = @_;
    return undef unless $authstr;
 
    my ($type, $enc) = split /\s+/, $authstr;
    return undef unless ($type eq 'Basic' && $enc);
 
    my ($username, $password) = split /:/, b64_decode($enc);
    return undef unless ($username && $password);
 
    { username => $username, password => $password };
}
 
1;
 
 
#--------------
#--- DAEMON ---
#--------------
 
package PGstore::Daemon;
 
use strict;
use warnings;
use POSIX qw(getpid setuid setgid geteuid getegid);
use Cwd qw(cwd getcwd chdir);
use Mojo::Util qw(dumper);
 
sub new {
    my $class = shift;
    my $self = {
        pid => undef
    };
    bless $self, $class;
    return $self;
}
 
sub fork {
    my ($self, $user, $group) = shift;
    my $pid = fork;
    if ($pid > 0) {
        exit;
    }
    chdir("/");
    open(my $stdout, '>&', STDOUT); 
    open(my $stderr, '>&', STDERR);
    open(STDOUT, '>>', '/dev/null');
    open(STDERR, '>>', '/dev/null');
    $self->pid(getpid);
    $self->pid;
}
 
sub pid {
    my ($self, $pid) = @_;
    return $self->{pid} unless $pid;
    $self->{pid} = $pid if $pid;
    $self;
}
 
1;
 
#-------------------
#--- CONTROLLER  ---
#-------------------
 
package PGstore::Controller;
 
use utf8;
use strict;
use warnings;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Util qw(md5_sum dumper quote encode url_unescape);
use Mojo::JSON qw(encode_json decode_json false true);
use File::Basename;
use Filesys::Df;
use File::stat;
use POSIX;
 
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 ||= 'application/octet-stream';
 
    # 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 hello {
    my $self = shift;
    $self->render(json => { message => 'hello', success => true });
}
 
sub data_list {
    my $self = shift;
 
    my $datadir = $self->app->config("datadir");
    return $self->render(json => { datalist => undef, success => false }) unless -d $datadir;
    return $self->render(json => { datalist => undef, success => false }) unless -r $datadir;
 
    opendir(my $dh, $datadir);
    my @list;
 
    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 $mtime = strftime("%Y-%m-%d %H:%M:%S %Z", localtime($st->mtime));
        push (@list, { name => $name, mtime => $mtime, size => $st->size });
 
    }
    closedir $dh;
    $self->render(
        json => { datalist => \@list, success => true }
    );
}
 
sub data_get {
    my $self = shift;
 
    my $dataname = $self->req->param('dataname');
    return $self->rendered(404) unless $dataname;
 
    $dataname = url_unescape($dataname);
 
    my $datadir = $self->app->config("datadir");
 
    my $file = "$datadir/$dataname";
    return $self->rendered(404) unless -r $file;
    return $self->rendered(404) unless -f $file;
    $self->renderFile(filepath => "$file");
}
 
sub data_put {
    my $self = shift;
 
    my $datadir = $self->app->config("datadir");
 
    return $self->render(json => { datalist => undef, success => false }) unless -d $datadir;
    return $self->render(json => { datalist => undef, success => false }) unless -w $datadir;
    return $self->render(json => { datalist => undef, success => false }) if $self->req->is_limit_exceeded;
 
    my @filenames;
 
    my $uploads = $self->req->uploads;
 
    foreach my $upload (@{$uploads}) {
        my $dataname = $upload->filename =~ s/^[ \.]+/_/gr;
        my $datasize = $upload->size;
 
        my $df = df("$datadir", 1);
        return $self->render(json => { success => false }) if $df->{bfree}+1 < $datasize;
 
        my $datafile = "$datadir/$dataname";
        $upload->move_to($datafile);
 
        my $st = stat($datafile);
        my $realsize = $st->size;
        do {
            unlink $dataname;
            next;
        } if $datasize != $realsize;
        push @filenames, { name => $dataname, size => $datasize, realsize => $realsize, realname => $datafile };
    }
 
    $self->render(json => { datalist => \@filenames, success => true });
}
 
sub data_free {
    my $self = shift;
    my $datadir = $self->app->config('datadir');
    return $self->render(json => { success => false }) unless -d $datadir;
    return $self->render(json => { success => false }) unless -r $datadir;
    my $df = df($datadir, 1);
    $self->render(json => { total => $df->{blocks}, free => $df->{bfree}, success => true });
 
}
 
sub data_delete {
    my $self = shift;
 
    my $datadir = $self->app->config('datadir');
    my $dataname = $self->req->param('dataname');
 
    return $self->render(json => { dataname => $dataname, success => false }) unless $dataname;
    return $self->render(json => { dataname => $dataname, success => false }) unless -d $datadir;
    return $self->render(json => { dataname => $dataname, success => false }) unless -w $datadir;
 
    my $datafile = "$datadir/$dataname";
    return $self->render(json => { dataname => $dataname, success => true, size => 0}) unless -f $datafile;
 
    my $st = stat($datafile);
    my $datasize = $st->size;
 
    return $self->render(json => { dataname => $dataname, success => true, size => $datasize }) if unlink($datafile);
    $self->render(json => { dataname => $dataname, success => false, size => $datasize });
}
 
1;
 
#-----------
#--- APP ---
#-----------
 
package PGstore;
 
use strict;
use warnings;
use Mojo::Base 'Mojolicious';
 
sub startup {
    my $self = shift;
}
 
1;
 
#-------------
#------------
#--- MAIN ---
#------------
#-------------
 
use strict;
use warnings;
 
use POSIX qw(setuid setgid tzset tzname strftime);
use Mojo::Server::Prefork;
use Mojo::IOLoop::Subprocess;
use Mojo::Util qw(md5_sum b64_decode getopt dumper);
use Sys::Hostname qw(hostname);
use File::Basename qw(basename dirname);
use Apache::Htpasswd;
use Cwd qw(getcwd abs_path);
use EV;
 
my $appname = 'pgdumper';
 
#--------------
#--- GETOPT ---
#--------------
 
getopt
    'h|help' => \my $help,
    'c|config=s' => \my $conffile,
    '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
    -c | --config=path    Path to config file
    -u | --user=user      System owner of process
    -g | --group=group    System group 
    -f | --nofork         Dont fork process
 
The options override options from configuration file
    )."\n";
    exit 0;
}
 
#------------------
#--- APP CONFIG ---
#------------------
 
my $server = Mojo::Server::Prefork->new;
my $app = $server->build_app('PGstore');
$app = $app->controller_class('PGstore::Controller');
 
$app->secrets(['6d578e43ba88260e0375a1a35fd7954b']);
$app->static->paths(['@APP_LIBDIR@/public']);
$app->renderer->paths(['@APP_LIBDIR@/templs']);
 
$app->config(conffile => $conffile || '@APP_CONFDIR@/pgstore.conf');
$app->config(pwdfile => '@APP_CONFDIR@/pgstore.pw');
$app->config(logfile => '@APP_LOGDIR@/pgstore.log');
$app->config(loglevel => 'info');
$app->config(pidfile => '@APP_RUNDIR@/pgstore.pid');
$app->config(crtfile => '@APP_CONFDIR@/pgstore.crt');
$app->config(keyfile => '@APP_CONFDIR@/pgstore.key');
 
$app->config(user => $user || '@APP_USER@');
$app->config(group => $group || '@APP_GROUP@');
 
$app->config(listenaddr4 => '0.0.0.0');
$app->config(listenaddr6 => '[::]');
$app->config(listenport => '3002');
 
$app->config(datadir => '@PGSTORE_DATADIR@');
 
 
if (-r $app->config('conffile')) {
    $app->log->debug("Load configuration from ".$app->config('conffile'));
    $app->plugin('JSONConfig', { file => $app->config('conffile') });
}
 
#---------------
#--- HELPERS ---
#---------------
 
$app->helper('reply.exception' => sub { my $c = shift; return $c->rendered(404); });
$app->helper('reply.not_found' => sub { my $c = shift; return $c->rendered(404); });
 
#--------------
#--- ROUTES ---
#--------------
 
my $r = $app->routes;
 
$r->add_condition(
    auth => sub {
        my ($route, $c) = @_;
        my $log = $c->app->log;
        my $authstr = $c->req->headers->authorization;
        my $pwdfile = $c->app->config('pwdfile');
        my $a = PGstore::BasicAuth->new($pwdfile);
        $log->info("Try auth user ". $a->username($authstr));
        $a->auth($authstr);
    }
);
 
$r->any('/hello')       ->to('controller#hello');
 
$r->any('/data/list')   ->over('auth') ->to('controller#data_list');
$r->any('/data/get')    ->over('auth') ->to('controller#data_get');
$r->any('/data/put')    ->over('auth') ->to('controller#data_put');
$r->any('/data/free')   ->over('auth') ->to('controller#data_free');
$r->any('/data/delete') ->over('auth') ->to('controller#data_delete');
 
#----------------
#--- LISTENER ---
#----------------
 
my $tls = '?';
$tls .= 'cert='.$app->config('crtfile');
$tls .= '&key='.$app->config('keyfile');
 
my $listen4;
if ($app->config('listenaddr4')) {
    $listen4 = "https://";
    $listen4 .= $app->config('listenaddr4').':'.$app->config('listenport');
    $listen4 .= $tls;
}
 
my $listen6;
if ($app->config('listenaddr6')) {
    $listen6 = "https://";
    $listen6 .= $app->config('listenaddr6').':'.$app->config('listenport');
    $listen6 .= $tls;
}
 
my @listen;
push @listen, $listen4 if $listen4;
push @listen, $listen6 if $listen6;
 
$server->listen(\@listen);
$server->heartbeat_interval(3);
$server->heartbeat_timeout(60);
 
#--------------
#--- DAEMON ---
#--------------
 
unless ($nofork) {
    my $d = PGstore::Daemon->new;
    my $user = $app->config('user');
    my $group = $app->config('group');
    $d->fork;
    $app->log(Mojo::Log->new( 
                path => $app->config('logfile'),
                level => $app->config('loglevel')
    ));
}
 
$server->pid_file($app->config('pidfile'));
 
#---------------
#--- WEB LOG ---
#---------------
 
$app->hook(before_dispatch => sub {
        my $c = shift;
 
        my $remote_address = $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;
        my $url = $c->req->url->to_abs->to_string;
 
        unless ($loglevel eq 'debug') {
            #$c->app->log->info("$remote_address $method $base$path");
            $c->app->log->info("$remote_address $method $url");
        }
        if ($loglevel eq 'debug') {
            $c->app->log->debug("$remote_address $method $url");
        }
});
 
#----------------------
#--- SIGNAL HANDLER ---
#----------------------
 
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;
#EOF

templs/agent_add.html.ep

templs/agent_add.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
 
% my $hostname = $req->param('hostname');
% my $username = $req->param('username');
% my $password = $req->param('password');
 
% my $m = $self->app->model;
% my $agent_id = $m->agent_exist($hostname);
 
% my $job_id = $m->job_create;
% $m->job_update(
%    $job_id, 
%    type => 'agentadd',
%    sourceid => $agent_id,
%    destid => $agent_id, 
%    author => $c->session('username')
% );
 
% if ($agent_id) {
    <div class="callout alert">
        Agent <%= $hostname %> yet exist.
    </div>
%   $m->job_update($job_id, status => 'mistake', error => 'agentexist', stop => $m->timestamp);
% }
 
% unless ($agent_id) {
%     my $agent_id = $m->agent_add($hostname, $username, $password);
%     if ($agent_id) {
        <div class="callout success">
        Agent <%= $hostname %> was added successful with id <%= $agent_id %>.
        </div>
        <div class="text-center">
          <a class="button" href="/agent/list">Agent List</a>
          <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
          <a class="button" href="#" data-open="modal-agent-add">Add yet</a>
        </div>
%       $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
%     }
 
%     unless ($agent_id) {
          <div class="callout alert">
          Agent <%= $hostname %> was added unsuccessful.
          </div>
          <div class="text-center">
              <a class="button" href="/agent/list">Agent List</a>
              <a class="button" href="#" data-open="modal-agent-add">Add yet</a>
          </div>
%         $m->job_update($job_id, status => 'error', error => 'agentadderr', stop => $m->timestamp);
%     }
% }
%#EOF

templs/agent_config.html.ep

templs/agent_config.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
 
% my $agent_id = $req->param('id');
% my $hostname = $req->param('hostname');
% my $username = $req->param('username');
% my $password = $req->param('password');
 
% my $m = $self->app->model;
% my $res = $m->agent_config($agent_id, $hostname, $username, $password);
 
% my $job_id = $m->job_create;
 
% $m->job_update(
%     $job_id,
%     type => 'agentreconf',
%     sourceid => $agent_id,
%     destid => $agent_id,
%     author => $c->session('username')
% );
 
% if ($res) {
    <div class="callout success">
        Agent <%= $hostname %> was updated successful.
    </div>
%   $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
% }
 
% unless ($res) {
    <div class="callout alert">
        Agent <%= $hostname %> was updated unsuccessful.
    </div>
%   $m->job_update($job_id, status => 'error', error => 'reconferror', stop => $m->timestamp);
% }
 
<div class="text-center">
    <a class="button" href="/agent/list">AgentList</a>
    <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
</div>
%#EOF

templs/agent_db_copy.html.ep

templs/agent_db_copy.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
% use Mojo::JSON qw(j encode_json decode_json true false);
 
% my $m = $self->app->model;
 
% my $dbname = $req->param('dbname') || undef;
% my $agent_id = $req->param('agentid') || undef;
% my $newname = $req->param('newname') || undef;
 
% my $agent_profile = $m->agent_profile($agent_id);
% my $agent_hostname = $agent_profile->{'hostname'} || 'undef';
% my $agent_username = $agent_profile->{'username'} || 'undef';
% my $agent_password = $agent_profile->{'password'} || 'undef';
 
% my $job_id = $m->job_create;
% $m->job_update(
%        $job_id, 
%        type => 'dbcopy', 
%        sourceid => $agent_id, 
%        destid => $agent_id, 
%        author => $c->session('username')
% );
% my $jobMagic = $m->job_profile($job_id)->{'magic'};
 
% my $ua = Mojo::UserAgent->new(max_redirects => 5);
 
% my $param = "dbname=$dbname";
% $param .= "&newname=$newname";
 
% my $tx = $ua->get("https://$agent_username:$agent_password\@$agent_hostname:3001/db/copy?$param");
 
% my $body;
% eval { $body = $tx->result->body };
% my $res = decode_json($body);
 
% if ($res->{success}) {
    <div class="callout success">
        Agent <%= $agent_hostname %> copy database <%= $dbname %> to <%= $newname %>  as job <%= $job_id %>.
    </div>
%   $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
%   $m->agent_db_update($agent_id);
% }
 
% unless ($res->{success}) {
    <div class="callout alert">
        Agent <%= $agent_hostname %> cannot copy <%= $dbname %>. 
        Job <%= $job_id %> was cancel.
    </div>
%   $m->job_update($job_id, status => 'pushjob', error => 'pusherr', stop => $m->timestamp);
% }
 
<div class="text-center">
    <a class="button" href="/agent/list">AgentList</a>
    <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
</div>
%#EOF

templs/agent_db_create.html.ep

templs/agent_db_create.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
% use Mojo::JSON qw(encode_json decode_json true false);
 
% my $m = $self->app->model;
 
% my $dbname = $req->param('dbname') || undef;
% my $agent_id = $req->param('agentid') || undef;
 
% my $agent_profile = $m->agent_profile($agent_id);
% my $agent_hostname = $agent_profile->{'hostname'} || 'undef';
% my $agent_username = $agent_profile->{'username'} || 'undef';
% my $agent_password = $agent_profile->{'password'} || 'undef';
 
% my $job_id = $m->job_create;
% $m->job_update(
%    $job_id, 
%    type => 'dbcreate',
%    sourceid => $agent_id,
%    destid => $agent_id, 
%    author => $c->session('username')
% );
% my $jobMagic = $m->job_profile($job_id)->{'magic'};
 
% my $ua = Mojo::UserAgent->new(max_redirects => 5);
% my $param = "dbname=$dbname";
 
% my $tx = $ua->get("https://$agent_username:$agent_password\@$agent_hostname:3001/db/create?$param");
 
% my $body;
% eval { $body = $tx->result->body };
% my $res = decode_json($body);
 
% if ($res->{success}) {
    <div class="callout success">
        Agent <%= $agent_hostname %> create database <%= $dbname %>  as job <%= $job_id %>.
    </div>
%   $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
%   $m->agent_db_update($agent_id);
% }
 
% unless ($res->{success}) {
    <div class="callout alert">
        Agent <%= $agent_hostname %> cannot create <%= $dbname %>. 
        Job <%= $job_id %> was cancel.
    </div>
%   $m->job_update($job_id, status => 'pushjob', error => 'pusherr', stop => $m->timestamp);
% }
 
<div class="text-center">
    <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
</div>
%#EOF

templs/agent_db_drop.html.ep

templs/agent_db_drop.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
% use Mojo::JSON qw(encode_json decode_json true false);
 
% my $m = $self->app->model;
 
% my $dbname = $req->param('dbname') || undef;
% my $agent_id = $req->param('agentid') || undef;
 
% my $agent_profile = $m->agent_profile($agent_id);
% my $agent_hostname = $agent_profile->{'hostname'} || 'undef';
% my $agent_username = $agent_profile->{'username'} || 'undef';
% my $agent_password = $agent_profile->{'password'} || 'undef';
 
% my $job_id = $m->job_create;
 
% $m->job_update(
%    $job_id,
%    type => 'dbdrop',
%    sourceid => $agent_id,
%    destid => $agent_id,
%    author => $c->session('username')
% );
% my $jobMagic = $m->job_profile($job_id)->{'magic'};
 
% my $ua = Mojo::UserAgent->new(max_redirects => 5);
% my $param = "dbname=$dbname";
% my $tx = $ua->get("https://$agent_username:$agent_password\@$agent_hostname:3001/db/drop?$param");
 
% my $body;
% eval { $body = $tx->result->body };
% my $res = decode_json($body);
 
% if ($res->{success}) {
    <div class="callout success">
        Agent <%= $agent_hostname %> drop database <%= $dbname %>  as job <%= $job_id %>.
    </div>
%   $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
%   $m->agent_db_update($agent_id);
% }
 
% unless ($res->{success}) {
    <div class="callout alert">
        Agent <%= $agent_hostname %> cannot drop <%= $dbname %>. 
        Job <%= $job_id %> was cancel.
    </div>
%   $m->job_update($job_id, status => 'pushjob', error => 'pusherr', stop => $m->timestamp);
%   $m->agent_db_update($agent_id);
% }
 
<div class="text-center">
    <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
</div>
%#EOF

templs/agent_db_dump.html.ep

templs/agent_db_dump.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
% use Mojo::JSON qw(encode_json decode_json true false);
 
% my $m = $self->app->model;
 
% my $dbname = $req->param('dbname') || undef;
% my $agent_id = $req->param('agentid') || undef;
% my $store_id = $req->param('storeid') || undef;
 
% my $agent_profile = $m->agent_profile($agent_id);
% my $agent_hostname = $agent_profile->{'hostname'} || '';
% my $agent_username = $agent_profile->{'username'} || '';
% my $agent_password = $agent_profile->{'password'} || '';
 
% my $store_profile = $m->store_profile($store_id);
% my $store_hostname = $store_profile->{'hostname'} || '';
% my $store_username = $store_profile->{'username'} || '';
% my $store_password = $store_profile->{'password'} || '';
 
% my $masterHostname = $self->app->config('hostname');
% my $job_id = $m->job_create;
 
% $m->job_update($job_id, type => 'dbdump', sourceid => $agent_id, destid => $store_id, author => $c->session('username'));
% my $jobMagic = $m->job_profile($job_id)->{'magic'};
 
% my $agent_alive = $m->agent_alive($agent_id);
 
% unless ($agent_alive) {
    <div class="callout alert">
        Oops...Agent <%= $agent_hostname %> not respond.
        Please, check agent configuration.
    </div>
%   $m->job_update($job_id, error => 'connecterr', stop => $m->timestamp);
% }
 
% my $store_alive = $m->store_alive($store_id);
% unless ($store_alive) {
    <div class="callout alert">
        Oops.. Store <%= $store_hostname %> not respond.
        Please, check store configuration.
    </div>
%   $m->job_update($job_id, error => 'connecterr', stop => $m->timestamp);
% }
 
% if ($agent_alive && $store_alive) {
%     my $ua = Mojo::UserAgent->new(max_redirects => 5);
 
%     my $param = "dbname=$dbname";
%     $param = $param."&store=$store_hostname";
%     $param = $param."&storelogin=$store_username";
%     $param = $param."&storepwd=$store_password";
%     $param = $param."&master=$masterHostname";
%     $param = $param."&jobid=$job_id";
%     $param = $param."&magic=$jobMagic";
 
%     $m->job_update($job_id, status => 'pushjob');
%     my $body = $ua->get("https://$agent_username:$agent_password\@$agent_hostname:3001/db/dump?$param")->result->body;
%     my $res = decode_json($body);
 
%     if ($res->{success}) {
        <div class="callout success">
            Agent <%= $agent_hostname %> now will create dump of database <%= $dbname %> as job <%= $job_id %>.
        </div>
%     }
%     unless ($res->{success}) {
        <div class="callout alert">
            Agent <%= $agent_hostname %> not correct responded for the request.
            Job <%= $job_id %> was cancel.
        </div>
%       $m->job_update($job_id, error => 'pushjoberr', stop => $m->timestamp);
%     }
% }
 
<div class="text-center">
    <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
    <a class="button" href="/store/data/list?id=<%= $store_id %>">DataList</a>
</div>
%#EOF

templs/agent_db_list.html.ep

templs/agent_db_list.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
 
 
% my $m = $self->app->model;
 
% my $agent_id = $req->param('id');
% my $agent_hostname = $m->agent_hostname($agent_id);
 
% my $agent_alive = $m->agent_alive($agent_id);
% unless ($agent_alive) {
    <div class="callout alert">
        Oops...Agent <%= $agent_hostname %> not responded.
        Use cached database list.
    </div>
% }
 
% if ($agent_alive) {
%    my $res = $m->agent_db_update($agent_id);
%    unless ($res) {
       <div class="callout alert">
          Oops...Agent <%= $agent_hostname %> not responded for update db list.
          Use cached database list.
      </div>
%    }
% }
 
% my $dbList = $m->agent_db_list($agent_id);
% my $store_list = $m->store_list;
 
% my $num = 1;
% foreach my $db (@{$dbList}) {
%   my $dbname = $db->{'name'};
%   my $dbsize = $db->{'size'};
%   my $dbowner = $db->{'owner'};
 
%#  --- dump database ---
    <div class="reveal" id="modal-dump-<%= $dbname %>" data-reveal>
        <div class="text-center">
            <h5>Dump database <%= $dbname %> from <%= $agent_hostname %></h5>
        </div>
        <form accept-charset="UTF-8" action="/agent/db/dump" method="get" data-abide novalidate>
            <input type="hidden" name="agentid" value="<%= $agent_id %>" />
            <input type="hidden" name="dbname" value="<%= $dbname %>" />
 
            <label>Store hostname
                <select name="storeid" id="store-hostname" required>
                    <option value=""></option>
%                   foreach my $store (@{$store_list}) {
%                      my $storeName = $store->{'hostname'};
%                      my $store_id = $store->{'id'};
                       <option value="<%= $store_id %>"><%= $storeName %></option>
%                   }
                </select>
            </label>
            <p class="text-center">
                <button type="submit" class="button success">Accept</button>
                <button class="button" data-close="modal-dump-<%= $dbname %>" type="button">Escape</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-dump-<%= $dbname %>" type="button">&times;</button>
    </div>
 
 
%#  --- schedule of dump database ---
    <div class="reveal" id="modal-schedule-<%= $dbname %>" data-reveal>
        <div class="text-center">
            <h5>Add dump schedule <%= $dbname %></h5>
        </div>
 
        <form accept-charset="UTF-8" action="/schedule/add" method="get" data-abide novalidate>
            <input type="hidden" name="type" value="dump" />
            <input type="hidden" name="sourceid" value="<%= $agent_id %>" />
            <input type="hidden" name="subject" value="<%= $dbname %>" />
 
            <label>Store hostname
                <select name="destid" required>
                    <option value=""></option>
%                   foreach my $store (@{$store_list}) {
%                      my $storeName = $store->{'hostname'};
%                      my $store_id = $store->{'id'};
                       <option value="<%= $store_id %>"><%= $storeName %></option>
%                   }
                </select>
            </label>
 
            <label>Day of month
                <input type="text" name="mday" value="1-31" placeholder="1-31" required pattern="[0-9\-;.,/*]{1,64}"/>
                <span class="form-error">mandatory, 1 or more letter, like 23,15-18</span>
            </label>
 
            <label>Day of week
                <input type="text" name="wday" value="1-7" placeholder="1-7" required pattern="[0-9\-,;./*]{1,64}"/>
                <span class="form-error">mandatory, 1 or more letter</span>
            </label>
 
            <label>Hour
                <input type="text" name="hour" value="6,23" placeholder="" required pattern="[0-9\-,;./*]{1,64}"/>
                <span class="form-error">mandatory, 1 or more letter</span>
            </label>
 
            <label>Min
                <input type="text" name="min" value="10" placeholder="" required pattern="[0-9\-,;./*]{1,64}"/>
                <span class="form-error">mandatory, 1 or more letter</span>
            </label>
 
            <p class="text-center">
                <button type="submit" class="button success">Accept</button>
                <button class="button" data-close="modal-schedule-<%= $dbname %>" type="button">Escape</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-schedule-<%= $dbname %>" type="button">&times;</button>
    </div>
 
 
%#  --- drop database ---
    <div class="reveal" id="modal-drop-<%= $dbname %>" data-reveal>
        <div class="text-center">
            <h5>Drop database <%= $dbname %> on <%= $agent_hostname %> </h5>
        </div>
        <form accept-charset="UTF-8" action="/agent/db/drop" method="get" data-abide novalidate>
            <input type="hidden" name="agentid" value="<%= $agent_id %>" />
            <input type="hidden" name="dbname" value="<%= $dbname %>" />
            <p class="text-center">
                <button type="submit" class="button alert">Accept</button>
                <button class="button" data-close="modal-drop-<%= $dbname %>" type="button">Escape</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-drop-<%= $dbname %>" type="button">&times;</button>
    </div>
 
%#  --- rename database ---
    <div class="reveal" id="modal-rename-<%= $dbname %>" data-reveal>
        <div class="text-center">
            <h5>Rename database <%= $dbname %> on <%= $agent_hostname %> </h5>
        </div>
        <form accept-charset="UTF-8" action="/agent/db/rename" method="get" data-abide novalidate>
            <input type="hidden" name="agentid" value="<%= $agent_id %>" />
            <input type="hidden" name="dbname" value="<%= $dbname %>" />
            <label>New name
                <input type="text" name="newname" value="<%= $dbname %>" placeholder="new db name" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{3,64}"/>
                <span class="form-error">mandatory, 3 or more letter</span>
            </label>
            <p class="text-center">
                <button type="submit" class="button success">Accept</button>
                <button class="button" data-close="modal-rename-<%= $dbname %>" type="button">Escape</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-rename-<%= $dbname %>" type="button">&times;</button>
    </div>
 
%#  --- copy database ---
    <div class="reveal" id="modal-copy-<%= $dbname %>" data-reveal>
        <div class="text-center">
            <h5>Copy database <%= $dbname %> on <%= $agent_hostname %> </h5>
        </div>
        <form accept-charset="UTF-8" action="/agent/db/copy" method="get" data-abide novalidate>
            <input type="hidden" name="agentid" value="<%= $agent_id %>" />
            <input type="hidden" name="dbname" value="<%= $dbname %>" />
            <label>Copy name
                <input type="text" name="newname" value="<%= $dbname %>_copy" placeholder="db name of copy" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{3,64}"/>
                <span class="form-error">mandatory, 3 or more letter</span>
            </label>
            <p class="text-center">
                <button type="submit" class="button success">Accept</button>
                <button class="button" data-close="modal-copy-<%= $dbname %>" type="button">Escape</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-copy-<%= $dbname %>" type="button">&times;</button>
    </div>
 
%#  --- operation modal ---
    <div class="reveal" id="modal-<%= $dbname %>" data-reveal>
        <div class="text-center">
            <h5><i class="fi-database" style="font-size:1.3em;"></i>&nbsp;Database <%= $dbname %> on <%= $agent_hostname %> </h5>
            <div class="stacked-for-small button-group">
                <a class="button" href="#" data-open="modal-schedule-<%= $dbname %>"><i class="fi-plus" style="font-size:1.3em;"></i>&nbsp;ToPlan</a>
                <a class="button" href="#" data-open="modal-dump-<%= $dbname %>"><i class="fi-archive" style="font-size:1.3em;"></i>&nbsp;Dump</a>
                <a class="button" href="#" data-open="modal-rename-<%= $dbname %>"><i class="fi-pencil" style="font-size:1.3em;"></i>&nbsp;Rename</a>
                <a class="button" href="#" data-open="modal-copy-<%= $dbname %>"><i class="fi-page-copy" style="font-size:1.3em;"></i>&nbsp;Copy</a>
                <a class="button alert" href="#" data-open="modal-drop-<%= $dbname %>"><i class="fi-trash" style="font-size:1.3em;"></i>&nbsp;Drop</a>
            </div>
        </div>
        <button class="close-button" data-close="modal-<%= $dbname %>" type="button">&times;</button>
    </div>
%   $num++;
% }
 
<div class="text-center">
    <h5>
        Agent <%= $agent_hostname%> database list&nbsp;
        <a href="/agent/db/list?id=<%= $agent_id %>"><i class="fi-refresh" style="font-size:1.3rem;"></i></a>
    </h5>
</div>
 
<table id="table" class="display" >
    <thead>
      <tr>
        <td>#</td>
        <td>dbname</td>
        <td>size</td>
        <td>conn</td>
        <td>owner</td>
      </tr>
    </thead>
    <tbody>
% $num = 1;
 
% foreach my $db (@{$dbList}) {
%   my $dbname = $db->{'name'};
%   my $dbsize = $db->{'size'};
%   my $dbowner = $db->{'owner'};
%   my $numbackends = $db->{'numbackends'};
%   $numbackends = '' if ($numbackends == 0);
    <tr>
        <td><%= $num %></td>
        <td><a href="#" data-open="modal-<%= $dbname %>"><%= $dbname %></a></td>
        <td><%= $dbsize %></td>
        <td><%= $numbackends  %></td>
        <td><%= $dbowner %></td>
    </tr>
%   $num++;
% }
    </tbody>
</table>
 
<script>
$.extend(true, $.fn.dataTable.defaults, {
    "searching": true,
    "ordering": true,
    "language": {
        "search": "",
        "lengthMenu": "_MENU_",
    },
 
} );
 
$(document).ready(function() {
    $('#table').DataTable();
});
</script>
 
%#EOF

templs/agent_db_rename.html.ep

templs/agent_db_rename.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
% use Mojo::JSON qw(encode_json decode_json true false);
 
% my $m = $self->app->model;
 
% my $dbname = $req->param('dbname') || undef;
% my $agent_id = $req->param('agentid') || undef;
% my $newname = $req->param('newname') || undef;
 
% my $agent_profile = $m->agent_profile($agent_id);
 
% my $agent_hostname = $agent_profile->{'hostname'} || '';
% my $agent_username = $agent_profile->{'username'} || 'undef';
% my $agent_password = $agent_profile->{'password'} || 'undef';
 
% my $job_id = $m->job_create;
% $m->job_update(
%     $job_id, 
%     type => 'dbrename',
%     sourceid => $agent_id,
%     destid => $agent_id
% );
% my $jobMagic = $m->job_profile($job_id)->{'magic'};
 
% my $ua = Mojo::UserAgent->new(max_redirects => 5);
 
% my $param = "dbname=$dbname";
% $param .= "&newname=$newname";
 
% my $tx = $ua->get("https://$agent_username:$agent_password\@$agent_hostname:3001/db/rename?$param");
 
% my $body;
% eval { $body = $tx->result->body };
% my $res = decode_json($body);
 
% if ($res->{success}) {
    <div class="callout success">
        Agent <%= $agent_hostname %> rename database <%= $dbname %> to <%= $newname %> as job <%= $job_id %>.
    </div>
%   $m->agent_db_update($agent_id);
%   $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
% }
 
% unless ($res->{success}) {
    <div class="callout alert">
        Agent <%= $agent_hostname %> cannot rename <%= $dbname %>. 
        Job <%= $job_id %> was cancel.
    </div>
%   $m->agent_db_update($agent_id);
%   $m->job_update($job_id, status => 'pushjob', error => 'pusherr', stop => $m->timestamp);
% }
 
<div class="text-center">
    <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
</div>
%#EOF

templs/agent_db_restore_.html.ep

templs/agent_db_restore_.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
% use Mojo::JSON qw(encode_json decode_json true false);
 
% my $m = $self->app->model;
 
% my $dbname = $req->param('dbname') || undef;
% my $dataname = $req->param('dataname') || undef;
% my $agent_id = $req->param('agentid') || undef;
% my $store_id = $req->param('storeid') || undef;
 
% my $newname = $req->param('newname') || undef;
 
% my $agent_profile = $m->agent_profile($agent_id);
% my $agent_hostname = $agent_profile->{'hostname'} || '';
% my $agent_username = $agent_profile->{'username'} || '';
% my $agent_password = $agent_profile->{'password'} || '';
 
% my $store_profile = $m->store_profile($store_id);
% my $store_hostname = $store_profile->{'hostname'} || '';
% my $store_username = $store_profile->{'username'} || '';
% my $store_password = $store_profile->{'password'} || '';
 
% my $masterHostname = $self->app->config('hostname');
% my $job_id = $m->job_create;
% $m->job_update(
%     $job_id,
%     type => 'dbrestore',
%     sourceid => $store_id,
%     destid => $agent_id
% );
% my $jobMagic = $m->job_profile($job_id)->{'magic'};
 
% my $agent_alive = $m->agent_alive($agent_id);
% unless ($agent_alive) {
    <div class="callout alert">
        Oops...Agent <%= $agent_hostname %> not respond.
        Please, check agent configuration.
    </div>
%   $m->job_update($job_id, error => 'connecterr', stop => $m->timestamp);
% }
 
% my $store_alive = $m->store_alive($store_id);
% unless ($store_alive) {
    <div class="callout alert">
        Oops.. Store <%= $store_hostname %> not respond.
        Please, check store configuration.
    </div>
%   $m->job_update($job_id, error => 'connecterr', stop => $m->timestamp);
% }
 
% if ($agent_alive && $store_alive) {
%     my $ua = Mojo::UserAgent->new(max_redirects => 5);
 
%     my $param = "dbname=$dbname";
%     $param .= "&dataname=$dataname";
%     $param .= "&store=$store_hostname";
%     $param .= "&storelogin=$store_username";
%     $param .= "&storepwd=$store_password";
%     $param .= "&newname=$newname";
 
%     $param .= "&master=$masterHostname";
%     $param .= "&jobid=$job_id";
%     $param .= "&magic=$jobMagic";
 
%     $m->job_update($job_id, status => 'pushjob');
%     my $tx = $ua->get("https://$agent_username:$agent_password\@$agent_hostname:3001/db/restore?$param");
 
%     my $body = '{"result":"mistake"}';
%     eval { $body = $tx->result->body };
%     my $res = decode_json($body);
 
%     if ( $res->{success} ) {
        <div class="callout success">
            Agent <%= $agent_hostname %> now restore database <%= $dbname %> as job <%= $job_id %>.
        </div>
%       $m->job_update($job_id, status => 'pushjob', error => 'noerr', stop => $m->timestamp);
%     }
 
%     if ( $res->{success} ) {
        <div class="callout alert">
            Agent <%= $agent_hostname %> not correct responded for the request.
            Job <%= $job_id %> was cancel.
        </div>
%       $m->job_update($job_id, status => 'pushjoberr', error => 'pushjoberr', stop => $m->timestamp);
%     }
% }
 
<div class="text-center">
    <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
    <a class="button" href="/store/data/list?id=<%= $store_id %>">DataList</a>
</div>
%#EOF

templs/agent_delete.html.ep

templs/agent_delete.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
 
% use Mojo::Util qw(dumper);
 
% my $m = $self->app->model;
 
% my $agent_id = $req->param('id');
% my $res = $self->app->model->agent_delete($agent_id);
 
% my $job_id = $m->job_create;
% $m->job_update(
%    $job_id, 
%    type => 'agentdelete',
%    sourceid => $agent_id,
%    destid => $agent_id, 
%    author => $c->session('username')
% );
 
% if ($res) {
    <div class="callout success">
        Agent id <%= $agent_id %> was deleted successful.
    </div>
%   $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
% }
 
% unless ($res) {
    <div class="callout alert">
        Agent id <%= $agent_id %> was deleted unsuccessful.
    </div>
%   $m->job_update($job_id, status => 'error', error => 'agentdelerr', stop => $m->timestamp);
% }
 
<div class="text-center">
    <a class="button" href="/agent/list">Agent List</a>
</div>
%#EOF

templs/agent_list.html.ep

templs/agent_list.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
 
% use Mojo::Util qw(b64_encode b64_decode md5_sum dumper);
 
% my $m = $self->app->model;
% my $agent_list = $m->agent_list;
 
% foreach my $agent (@{$agent_list}) {
%   my $agent_id = $agent->{'id'};
%   my $hostname = $agent->{'hostname'};
%   my $username = $agent->{'username'};
%   my $password = $agent->{'password'};
 
    <div class="reveal" id="modal-agent-config-<%= $agent_id %>" data-reveal>
        <form accept-charset="UTF-8" action="/agent/config" method="get" data-abide novalidate>
 
            <h5>Configuration agent <%= $hostname %></h5>
            <input type="hidden" name="id" value="<%= $agent_id %>"/>
            <label>hostname
                <input type="text" name="hostname" placeholder="hostname" value="<%= $hostname %>" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{5,42}"/>
                <span class="form-error">mandatory parameter, 5 or more letter</span>
            </label>
            <label>username
                <input type="text" name="username" placeholder="username" value="<%= $username %>" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{5,42}"/>
                <span class="form-error">mandatory, 5 or more letter</span>
            </label>
            <label>password
                <input type="password" name="password" placeholder="password" value="<%= $password %>" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{5,42}"/>
                <span class="form-error">mandatory, 5 or more letter</span>
            </label>
            <p class="text-center">
                <button type="submit" class="button success">Accept</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-agent-config-<%= $agent_id %>" type="button">&times;</button>
    </div>
 
    <div class="reveal" id="modal-agent-delete-<%= $agent_id %>" data-reveal>
        <div class="text-center">
            <h5>Do you want to delete agent <%= $hostname %>?</h5>
            <a class="button" data-close="modal-agent-delete-<%= $agent_id %>">Escape</a>
            <a class="button alert" href="/agent/delete?id=<%= $agent_id %>">Delete</a>
        </div>
        <button class="close-button" data-close="modal-agent-delete-<%= $agent_id %>" type="button">&times;</button>
    </div>
 
    <div class="reveal" id="modal-agent-db-create-<%= $agent_id %>" data-reveal>
        <h5>Create new database on agent [<%= $hostname %>] </h5>
        <form accept-charset="UTF-8" action="/agent/db/create" method="get" data-abide novalidate>
            <input type="hidden" name="agentid" value="<%= $agent_id %>" />
            <label>New name
                <input type="text" name="dbname" value="new_empty_db" placeholder="empty_db" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{3,64}"/>
                <span class="form-error">mandatory, 3 or more letter</span>
            </label>
            <p class="text-center">
                <button type="submit" class="button">Accept</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-agent-db-create-<%= $agent_id %>" type="button">&times;</button>
    </div>
 
    <div class="reveal" id="modal-agent-<%= $agent_id %>" data-reveal>
        <div class="text-center">
            <h5>Agent <%= $hostname %></h5>
            <div class="stacked-for-small button-group">
                <a class="button" href="#" data-open="modal-agent-config-<%= $agent_id %>">ReConf</a>
                <a class="button" href="/agent/db/list?id=<%= $agent_id %>">DBList</a>
                <a class="button" href="#" data-open="modal-agent-db-create-<%= $agent_id %>">DBCreate</a>
                <a class="button alert" href="#" data-open="modal-agent-delete-<%= $agent_id %>">Delete</a>
            </div>
        </div>
        <button class="close-button" data-close="modal-agent-<%= $agent_id %>" aria-label="Close modal" type="button">
            <span aria-hidden="true">&times;</span>
        </button>
    </div>
 
 
% }
 
<div class="text-center">
    <h5>Agent list&nbsp;<a href="/agent/list"><i class="fi-refresh" style="font-size:1.3rem;"></i></a></h5>
</div>
 
<table id="table" class="display" >
    <thead>
      <tr>
        <td>#</td>
        <td>hostname</td>
        <td>count db</td>
        <td>total</td>
      </tr>
    </thead>
    <tbody>
% my $num = 1;
% foreach my $agent (@{$agent_list}) {
%   my $agent_id = $agent->{'id'};
%   my $hostname = $agent->{'hostname'};
%   my $agent_info = $m->agent_info($agent_id);
%   my $sum = $agent_info->{'sum'} || 0;
%   my $count = $agent_info->{'count'} || 0;
    <tr>
        <td><%= $num %></td>
        <td><a href="#" data-open="modal-agent-<%= $agent_id %>"><%= $hostname %></a></td>
        <td><%= $count %></td>
        <td><%= $m->size_m($sum) %></td>
    </tr>
%   $num++;
% }
    </tbody>
</table>
%#EOF

templs/data_list.html.ep

templs/data_list.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
 
 
% my $data_list = $self->app->model->data_list;
 
<div class="text-center">
    <h5>Generic dataset list</h5>
</div>
 
<table id="table" class="display" >
    <thead>
      <tr>
        <td>#</td>
        <td>store</td>
        <td>dbdump</td>
        <td>date</td>
        <td>size</td>
        <td>source</td>
      </tr>
    </thead>
    <tbody>
% my $num = 1;
% foreach my $data (@{$data_list}) {
%   my $filename = $data->{'name'};
%   my $dbname = $data->{'dbname'};
%   my $stamp = $data->{'stamp'};
%   my $size = $data->{'size'};
%   my $store = $data->{'store'};
%   my $store_id = $data->{'storeid'};
%   my $source = $data->{'source'};
    <tr>
        <td><%= $num %></td>
        <td><a href="/store/data/list?id=<%= $store_id %>"><%= $store %></a></td>
        <td><%= $dbname %></td>
        <td><%= $stamp %></td>
        <td><%= $size %></td>
        <td><%= $source %></td>
    </tr>
%   $num++;
% }
    </tbody>
</table>
 
<script>
$.extend(true, $.fn.dataTable.defaults, {
    "searching": true,
    "ordering": true,
    "lengthChange": false,
    "language": {
        "search": "",
        "lengthMenu": "_MENU_",
    },
});
 
$(document).ready(function() {
    $('#table').DataTable({
        "info": false,
        "processing": true
 
    });
});
</script>
 
 
%#EOF

templs/exception.development.html.ep

templs/exception.development.html.ep
%#
%# $Id: exception.html.ep 627 2017-04-15 13:02:08Z ziggi $
%#
% layout 'default';
% title 'Error';
 
<h5>Oops... Exception</h5>
 
<pre>  
%= $exception
</pre>
 
%#EOF

templs/exception.production.html.ep

templs/exception.production.html.ep
%#
%# $Id: exception.html.ep 627 2017-04-15 13:02:08Z ziggi $
%#
% layout 'default';
% title 'Error';
 
<h5>Oops... Exception</h5>
 
<pre>
%= $exception
</pre>
 
%#EOF

templs/hello.html.ep

templs/hello.html.ep
% layout 'default';
% title 'Dumper Hello';
% use Mojo::Util qw(b64_encode b64_decode md5_sum dumper);
% use Encode qw(decode encode);
 
 
<h5 class="text-center">Hello, body! How are you there?</h5>
 
%#EOF

templs/job_list.html.ep

templs/job_list.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Switch;
% use Mojo::Util qw(dumper);
 
% my $m = $self->app->model;
% my $job_list = $m->job_list;
 
<div class="text-center">
    <h5>
        Job list
        <a href="/job/list"><i class="fi-refresh" style="font-size:1.3rem;"></i></a>
    </h5>
</div>
 
<table id="table" class="display" >
    <thead>
      <tr>
        <td>#</td>
        <td>job#</td>
        <td>type</td>
        <td>status</td>
        <td>err</td>
        <td>source</td>
        <td>dest</td>
        <td>start</td>
        <td>last</td>
        <td>author</td>
      </tr>
    </thead>
    <tbody>
% my $num = 1;
% foreach my $job (@{$job_list}) {
%   my $job_id = $job->{'id'};
%   my $type = $job->{'type'};
%   my $source_id = $job->{'sourceid'};
%   my $dest_id = $job->{'destid'};
%   my $sourcehost = '';
%   my $desthost = '';
%   my $status = $job->{'status'};
%   my $start = $job->{'begin'};
%   my $stop = $job->{'stop'};
%   my $error = $job->{'error'} || 'noerr';
%   my $author = $job->{'author'} || 'unkn';
 
%   if ($type eq 'dbdump') { 
%      $sourcehost = $m->agent_profile($source_id)->{'hostname'} if $source_id;
%      $desthost = $m->store_profile($dest_id)->{'hostname'} if $dest_id;
%   }
 
%   if ($type eq 'dbcopy' || $type eq 'dbrename' || $type eq 'dbdrop') { 
%      $sourcehost = $m->agent_profile($source_id)->{'hostname'} if $source_id;
%      $desthost = '';
%   }
 
%  $error = '' if ($error eq 'noerr' || $error eq 'undef');
%  $stop = '' if ($stop =~ /1970-01-01/);
 
    <tr>
        <td><%= $num %></td>
        <td><%= $job_id %></td>
        <td><%= $type %></td>
        <td><%= $status %></td>
        <td><%= $error %></td>
        <td><%= $sourcehost %></td>
        <td><%= $desthost %></td>
        <td><%= $start %></td>
        <td><%= $stop %></td>
        <td><%= $author %></td>
    </tr>
%   $num++;
% }
    </tbody>
</table>
 
<script>
$.extend(true, $.fn.dataTable.defaults, {
    "searching": true,
    "ordering": true,
    "lengthChange": true,
    "language": {
        "search": "",
        "lengthMenu": "_MENU_",
    },
});
 
%#$(document).ready(function() {
%#    $('#table').DataTable({
%#        "info": false,
%#        "processing": true
%#    });
%#});
</script>
 
%#<script>
%#var timeout = setTimeout("location.reload(true);",6000);
%#function resetTimeout() {
%#    clearTimeout(timeout);
%#    timeout = setTimeout("location.reload(true);",6000);
%#}
%#</script>
 
%#EOF

templs/login.html.ep

templs/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="row">&nbsp;</div>
        <div class="row">
            <div class="small-3 columns hide-for-small">&nbsp;</div>
            <div class="small-6 columns text-center">
                <div class="row">
                    <div class="columns">
 
                        <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="username" />
                                </label>
                                <label>_password
                                    <input type="password" name="password" placeholder="password" />
                                </label>
                                <p>
                                    <button type="submit" class="button">Log In</button>
                                </p>
                                <p class="text-center"></p>
                            </div>
                        </form>
 
                    </div>
                </div>
            </div>
            <div class="small-3 columns hide-for-small">&nbsp;</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>
<!- EOF ->
%# EOF

templs/not_found.development.html.ep

templs/not_found.development.html.ep
%#
%# $Id: not_found.html.ep 627 2017-04-15 13:02:08Z ziggi $
%#
% layout 'default';
% title 'Dumper';
 
<h5>404 Page not found</h5>
 
%#EOF

templs/not_found.production.html.ep

templs/not_found.production.html.ep
%#
%# $Id: not_found.html.ep 627 2017-04-15 13:02:08Z ziggi $
%#
% layout 'default';
% title 'Dumper';
 
<h5>404 Page not found</h5>
 
%#EOF

templs/schedule_add.html.ep

templs/schedule_add.html.ep
%#
%# $Id$
%#
% use Mojo::Util qw(dumper);
 
% layout 'default';
% title 'Dumper';
 
% my $type = $req->param('type');
% my $source_id = $req->param('sourceid');
% my $dest_id = $req->param('destid');
% my $subject = $req->param('subject');
% my $mday = $req->param('mday');
% my $wday = $req->param('wday');
% my $hour = $req->param('hour');
% my $min = $req->param('min');
 
% my $m = $self->app->model;
 
% my $job_id = $m->job_create;
 
% $m->job_update(
%    $job_id, 
%    type => 'scheduleadd',
%    sourceid => $source_id,
%    destid => $dest_id, 
%    author => $c->session('username')
% );
 
% my $schedule_id = $m->schedule_add($type, $source_id, $dest_id, $subject, $mday, $wday, $hour, $min);
 
% if ($schedule_id) {
      <div class="callout success">
        Schedule record was added successful with id <%= $schedule_id %>.
      </div>
      $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
% }
 
% unless($schedule_id) {
      <div class="callout alert">
          Schedule record was added unsuccessful =(
      </div>
      $m->job_update($job_id, status => 'error', error => 'shcheduleadderr', stop => $m->timestamp);
% }
 
%#EOF

templs/schedule_list.html.ep

templs/schedule_list.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
 
 
% my $m = $self->app->model;
% my $schedule_list = $m->schedule_list;
 
<div class="text-center">
    <h5>
    Schedule list&nbsp;<a href="/schedule/list"><i class="fi-refresh" style="font-size:1.3rem;"></i></a>
    </h5>
</div>
 
<table id="table" class="display" >
    <thead>
      <tr>
        <td>#</td>
        <td>type</td>
        <td>source</td>
        <td>dest</td>
        <td>subj</td>
        <td>mday</td>
        <td>wday</td>
        <td>hour</td>
        <td>min</td>
      </tr>
    </thead>
    <tbody>
% my $num = 1;
% foreach my $descr (@{$schedule_list}) {
%   my $type = $descr->{'type'};
%   my $source_id = $descr->{'sourceid'};
%   my $dest_id = $descr->{'destid'};
%   my $subject = $descr->{'subject'};
%   my $mday = $descr->{'mday'};
%   my $wday = $descr->{'wday'};
%   my $hour = $descr->{'hour'};
%   my $min = $descr->{'min'};
 
%   my $source = 'undef';
%   my $dest = 'undef';
 
%   if ($type eq 'dump') { 
%        $source = $m->agent_hostname($source_id) || 'undef';
%        $dest = $m->store_hostname($dest_id) || 'undef'; 
%   }
    <tr>
        <td><%= $num %></td>
        <td><%= $type %></td>
        <td><%= $source %></td>
        <td><%= $dest %></td>
        <td><%= $subject %></td>
        <td><%= $mday %></td>
        <td><%= $wday %></td>
        <td><%= $hour %></td>
        <td><%= $min %></td>
    </tr>
%   $num++;
% }
    </tbody>
</table>
 
<script>
$.extend(true, $.fn.dataTable.defaults, {
    "searching": true,
    "ordering": true,
    "lengthChange": true,
    "language": {
        "search": "",
        "lengthMenu": "_MENU_",
    },
});
 
$(document).ready(function() {
    $('#table').DataTable({
        "info": false,
        "processing": true
    });
});
</script>
%#EOF

templs/store_add.html.ep

templs/store_add.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
 
% my $hostname = $req->param('hostname');
% my $username = $req->param('username');
% my $password = $req->param('password');
 
% my $m = $self->app->model;
% my $store_id = $m->store_exist($hostname);
 
% my $job_id = $m->job_create;
 
% $m->job_update(
%    $job_id, 
%    type => 'storeadd',
%    sourceid => $store_id,
%    destid => $store_id, 
%    author => $c->session('username')
% );
 
% if ($store_id) {
    <div class="callout alert">
        Store <%= $hostname %> yet exist.
    </div>
%   $m->job_update($job_id, status => 'mistake', error => 'storeexist', stop => $m->timestamp);
% }
 
% unless ($store_id) {
%   my $store_id = $self->app->model->store_add($hostname, $username, $password);
%   if ($store_id) {
      <div class="callout success">
        Store <%= $hostname %> was added successful with id <%= $store_id %>.
      </div>
%     $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
%   }
 
%   unless ($store_id) {
      <div class="callout alert">
          Store <%= $hostname %> was added unsuccessful =(
      </div>
%     $m->job_update($job_id, status => 'error', error => 'storeadderr', stop => $m->timestamp);
%   }
% }
 
<div class="text-center">
    <a class="button" href="/store/list">StoreList</a>
    <a class="button" href="/store/data/list?id=<%= $store_id %>">DataList</a>
    <a class="button" href="#" data-open="modal-store-add">Add yet</a>
</div>
%#EOF

templs/store_config.html.ep

templs/store_config.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
 
% my $m = $self->app->model;
 
% my $store_id = $req->param('id');
% my $hostname = $req->param('hostname');
% my $username = $req->param('username');
% my $password = $req->param('password');
 
% my $res = $m->store_config($store_id, $hostname, $username, $password);
 
% my $job_id = $m->job_create;
 
% $m->job_update(
%     $job_id,
%     type => 'storereconf',
%     sourceid => $store_id,
%     destid => $store_id,
%     author => $c->session('username')
% );
 
% if ($res) {
    <div class="callout success">
        Store <%= $hostname %> was updated successful.
    </div>
%   $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
% }
 
% unless ($res) {
    <div class="callout alert">
        Store <%= $hostname %> was updated unsuccessful.
    </div>
%   $m->job_update($job_id, status => 'error', error => 'reconferror', stop => $m->timestamp);
% }
 
<div class="text-center">
    <a class="button" href="/store/list">StoreList</a>
    <a class="button" href="/store/data/list?id=<%= $store_id %>">DataList</a>
</div>
%#EOF

templs/store_data_delete.html.ep

templs/store_data_delete.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper url_escape);
% use Mojo::JSON qw(decode_json);
 
 
% my $m = $self->app->model;
 
% my $store_id = $req->param('id');
% my $dataname = $req->param('dataname');
 
% my $store_profile = $m->store_profile($store_id);
% my $store_hostname = $store_profile->{'hostname'};
% my $store_username = $store_profile->{'username'};
% my $store_password = $store_profile->{'password'};
 
% my $job_id = $m->job_create;
% $m->job_update(
%     $job_id,
%     type => 'dropdata',
%     sourceid => $store_id,
%     destid => $store_id,
%     author => $c->session('username')
% );
 
% my $ua = Mojo::UserAgent->new(max_redirects => 5);
 
% my $dataname_enc = url_escape $dataname;
% my $param = "dataname=$dataname_enc";
 
% my $tx = $ua->get("https://$store_username:$store_password\@$store_hostname:3002/data/delete?$param");
 
% my $body;
% eval { $body = $tx->result->body };
% my $res = decode_json($body);
 
% if ($res->{success}) {
    <div class="callout success">
        Store <%= $store_hostname %> deleted <%= $dataname %> as job <%= $job_id %>.
    </div>
%   $m->store_data_update($store_id);
%   $m->job_update($job_id, status => 'deleted', error => 'noerr', stop => $m->timestamp);
% }
 
% unless ($res->{success}) {
    <div class="callout alert">
        Store <%= $store_hostname %> cannot delete <%= $dataname %>. 
        Job <%= $job_id %> was cancel.
    </div>
%   $m->job_update($job_id, status => 'pushjob', error => 'pusherr', stop => $m->timestamp);
% }
 
<div class="text-center">
    <a class="button" href="/store/list">StoreList</a>
    <a class="button" href="/store/data/list?id=<%= $store_id %>">DataList</a>
</div>
%#EOF

templs/store_data_list.html.ep

templs/store_data_list.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper md5_sum url_escape);
 
 
% sub _stripDate {
%   my $a = substr shift, 0, 16;
%   $a =~ s/\//-/g;
%   return $a;
% }
 
% my $m = $self->app->model;
 
% my $store_id = $req->param('id');
% my $store_hostname = $m->store_hostname($store_id);
 
% my $store_alive = $m->store_alive($store_id);
% unless ($store_alive) {
    <div class="callout alert">
        Oops.. Store <%= $store_hostname %> not respond.
        Please, check store configuration. 
        Now will use cached dataset list.
    </div>
% }
 
% if ($store_alive) {
%    my $res = $m->store_data_update($store_id);
%    unless ($res) {
       <div class="callout alert">
          Oops...Store <%= $store_hostname %> not responded for update data list.
          Use cached data list.
      </div>
%    }
% }
 
% my $data_list = $m->store_data_list($store_id);
% my $agent_list = $m->agent_list;
 
 
% my $num = 1;
% foreach my $data (@{$data_list}) {
%   my $dataname = $data->{'name'};
%   my $dbname = $data->{'dbname'};
%   my $size = $data->{'size'};
%   my $source = $data->{'source'};
%   my $stamp = $data->{'datetime'} || $data->{'stamp'} ;
 
%   my $newName = $dbname."-".$m->strip_sec($stamp);
%   $newName =~ s/[-\.: ]//g;
%   $newName =~ s/[+]//g;
 
%   my $modal_id = md5_sum($dataname);
 
    <div class="reveal" id="modal-data-restore-<%= $modal_id %>" data-reveal>
 
        <div class="text-center">
            <h5>Restore database <%= $dbname %> as <%= $newName %></h5>
        </div>
 
        <form accept-charset="UTF-8" action="/agent/db/restore" method="get" data-abide novalidate>
            <input type="hidden" name="storeid" value="<%= $store_id %>" />
            <input type="hidden" name="dataname" value="<%= $dataname %>" />
            <input type="hidden" name="dbname" value="<%= $newName %>" />
 
            <label>Agent hostname
                <select name="agentid" id="agent-hostname" required>
                    <option value=""></option>
%                   foreach my $agent (@{$agent_list}) {
%                      my $agentName = $agent->{'hostname'};
%                      my $agent_id = $agent->{'id'};
                       <option value="<%= $agent_id %>"><%= $agentName %></option>
%                   }
                </select>
            </label>
 
            <label>New name
                <input type="text" name="newname" value="<%= $newName %>" placeholder="new db name" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{3,64}"/>
                <span class="form-error">mandatory, 3 or more letter</span>
            </label>
 
            <p class="text-center">
                <button type="submit" class="button success">Accept</button>
                <button class="button" data-close="modal-data-restore-<%= $modal_id %>" type="button">Escape</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-data-restore-<%= $modal_id %>" type="button">&times;</button>
    </div>
 
    <div class="reveal" id="modal-data-delete-<%= $modal_id %>" data-reveal>
        <div class="text-center">
            <h5>Do you want to delete the data?</h5>
            <a class="button" data-close="modal-data-delete-<%= $modal_id %>">Escape</a>
            <a class="button alert" href="/store/data/delete?id=<%= $store_id %>&dataname=<%=  url_escape $dataname %>">Delete</a>
        </div>
        <button class="close-button" data-close="modal-data-delete-<%= $modal_id %>" type="button">&times;</button>
    </div>
 
    <div class="reveal" id="modal-store-data-<%= $modal_id %>" data-reveal>
        <div class="text-center">
            <h5>Store <%= $store_hostname %></h5>
            <div class="stacked-for-small button-group">
                <a class="button" href="#" data-open="modal-data-restore-<%= $modal_id %>">Restore</a>
                <a class="button alert" href="#" data-open="modal-data-delete-<%= $modal_id %>">Delete</a>
            </div>
        </div>
        <button class="close-button" data-close="modal-store-data-<%= $modal_id %>" aria-label="Close modal" type="button">
            <span aria-hidden="true">&times;</span>
        </button>
    </div>
 
% }
 
<div class="text-center">
    <h5>
        Store <%= $store_hostname%> dataset list&nbsp;
        <a href="/store/data/list?id=<%= $store_id %>">
            <i class="fi-refresh" style="font-size:1.3rem;"></i>
        </a>
    </h5>
</div>
 
<table id="table" class="display" >
    <thead>
      <tr>
        <td>#</td>
        <td>dbdump</td>
        <td class="show-for-medium">local date</td>
        <td class="show-for-large">orig date</td>
        <td>size</td>
        <td class="show-for-medium">source</td>
      </tr>
    </thead>
    <tbody>
 
% $num = 1;
% foreach my $data (@{$data_list}) {
%   my $filename = $data->{'name'};
%   my $dataname = $data->{'name'};
%   my $dbname = $data->{'dbname'};
%   my $stamp = $data->{'stamp'};
%   my $size = $data->{'size'};
%   my $datetime = $data->{'datetime'};
%   my $tz = $data->{'tz'};
%   my $source = $data->{'source'};
 
%   my $modal_id = md5_sum($dataname);
    <tr>
        <td><%= $num %></td>
        <td><a href="#" data-open="modal-store-data-<%= $modal_id %>"><%= $dbname %></a></td>
        <td class="show-for-medium"><%= $m->strip_sec($stamp) %></td>
        <td class="show-for-large"><%= $m->strip_sec($datetime)." ".$tz %></td>
        <td><%= $m->size_m($size) %></td>
        <td class="show-for-medium"><%= $source %></td>
    </tr>
%   $num++;
% }
    </tbody>
</table>
 
<script>
$.extend(true, $.fn.dataTable.defaults, {
    "searching": true,
    "ordering": true,
    "lengthChange": true,
    "language": {
        "search": "",
        "lengthMenu": "_MENU_",
    },
} );
 
$(document).ready(function() {
    $('#table').DataTable();
});
 
</script>
%#EOF

templs/store_delete.html.ep

templs/store_delete.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
% use Encode qw(decode encode);
 
% my $m = $self->app->model;
% my $store_id = $req->param('id');
% my $dataname = $req->param('dataname');
 
% my $job_id = $m->job_create;
 
% $m->job_update(
%    $job_id, 
%    type => 'storedelete',
%    sourceid => $store_id,
%    destid => $store_id, 
%    author => $c->session('username')
% );
 
% my $res = $m->store_delete($store_id);
 
% if ($res) {
    <div class="callout success">
        Store id <%= $store_id %> was deleted successful.
    </div>
%   $m->job_update($job_id, status => 'done', error => 'noerr', stop => $m->timestamp);
% };
 
% unless ($res) {
    <div class="callout alert">
        Store id <%= $store_id %> was deleted unsuccessful.
    </div>
%   $m->job_update($job_id, status => 'error', error => 'storedelerr', stop => $m->timestamp);
% }
 
<div class="text-center">
    <a class="button" href="/store/list">Store List</a>
</div>
%#EOF

templs/store_list.html.ep

templs/store_list.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(b64_encode b64_decode md5_sum dumper);
% use Encode qw(decode encode);
 
 
% my $m = $self->app->model;
% my $store_list = $m->store_list;
 
% foreach my $store (@{$store_list}) {
%   my $store_id = $store->{'id'};
%   my $hostname = $store->{'hostname'};
%   my $username = $store->{'username'};
%   my $password = $store->{'password'};
%   my $free = $m->store_free($store_id) || 0; 
 
    <div class="reveal" id="modal-store-config-<%= $store_id %>" data-reveal>
        <form accept-charset="UTF-8" action="/store/config" method="get" data-abide novalidate>
 
            <h5>Configuration agent <%= $hostname %></h5>
            <input type="hidden" name="id" value="<%= $store_id %>"/>
            <label>hostname
                <input type="text" name="hostname" placeholder="hostname" value="<%= $hostname %>" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{5,42}"/>
                <span class="form-error">mandatory parameter, 5 or more letter</span>
            </label>
            <label>username
                <input type="text" name="username" placeholder="username" value="<%= $username %>" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{5,42}"/>
                <span class="form-error">mandatory, 5 or more letter</span>
            </label>
            <label>password
                <input type="password" name="password" placeholder="password" value="<%= $password %>" required pattern="[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{5,42}"/>
                <span class="form-error">mandatory, 5 or more letter</span>
            </label>
            <p class="text-center">
                <button type="submit" class="button success">Accept</button>
            </p>
        </form>
        <button class="close-button" data-close="modal-store-config-<%= $store_id %>" type="button">&times;</button>
    </div>
 
    <div class="reveal" id="modal-store-<%= $store_id %>" data-reveal>
        <div class="text-center">
            <h5>Store <%= $hostname %></h5>
            <div class="stacked-for-small button-group">
                <a class="button" href="#" data-open="modal-store-config-<%= $store_id %>">ReConf</a>
                <a class="button" href="/store/data/list?id=<%= $store_id %>">DataList</a>
                <a class="button alert" href="#" data-open="modal-store-delete-<%= $store_id %>">Delete</a>
            </div>
        </div>
        <button class="close-button" data-close="modal-store-<%= $store_id %>" type="button">&times;</button>
    </div>
 
    <div class="reveal" id="modal-store-delete-<%= $store_id %>" data-reveal>
        <div class="text-center">
            <h5>Do you want to delete store <%= $hostname %>?</h5>
            <a class="button" data-close="modal-store-delete-<%= $store_id %>">Escape</a>
            <a class="button alert" href="/store/delete?id=<%= $store_id %>">Delete</a>
        </div>
        <button class="close-button" data-close="modal-store-delete-<%= $store_id %>" type="button">&times;</button>
    </div>
 
% }
 
<h5 class="text-center">Store list&nbsp;<a href="/store/list"><i class="fi-refresh" style="font-size:1.3rem;"></i></a></h5>
 
<table id="table" class="display" >
    <thead>
      <tr>
        <td>#</td>
        <td>hostname</td>
        <td>count</td>
        <td>total</td>
        <td>free</td>
      </tr>
    </thead>
    <tbody>
% my $num = 1;
% foreach my $store (@{$store_list}) {
%   my $store_id = $store->{'id'};
%   my $hostname = $store->{'hostname'};
 
%   my $store_info = $m->store_info($store_id);
%   my $sum = $store_info->{'sum'};
%   my $count = $store_info->{'count'};
 
%   my $free = $m->store_free($store_id) || 0; 
    <tr>
        <td><%= $num %></td>
        <td><a href="#" data-open="modal-store-<%= $store_id %>"><%= $hostname %></a></td>
        <td><a href="/store/data/list?id=<%= $store_id %>"><%= $count %></a></td>
        <td><%= $m->size_m($sum) if $sum %></td>
        <td><%= $m->size_m($free) if $free %></td>
    </tr>
%   $num++;
% }
    </tbody>
</table>
%#EOF

templs/template.html.ep

templs/template.html.ep
%#
%# $Id$
%#
% layout 'default';
% title 'Dumper';
% use Mojo::Util qw(dumper);
 
 
%#EOF

First PagePrevious PageBack to overviewNext PageLast Page