App-Web-VPKBuilder
view release on metacpan or search on metacpan
lib/App/Web/VPKBuilder.pm view on Meta::CPAN
use File::Spec::Functions qw/abs2rel catfile rel2abs/;
use File::Temp qw/tempdir/;
use IO::Compress::Zip qw/zip ZIP_CM_LZMA/;
use sigtrap qw/die normal-signals/;
use Data::Diver qw/DiveRef/;
use File::Slurp qw/write_file/;
use HTML::Element;
use HTML::TreeBuilder;
use Hash::Merge qw/merge/;
use List::MoreUtils qw/uniq/;
use Plack::Request;
use Sort::ByExample qw/sbe/;
use YAML qw/LoadFile/;
sub new {
my $self = shift->SUPER::new(@_);
$self->{cfg} = {};
for (sort <cfg/*>) {
my $cfg = LoadFile $_;
$self->{cfg} = merge $self->{cfg}, $cfg
}
$self->{cfg}{vpk_extension} //= 'vpk';
$self->{cfg}{sort} = sbe $self->{cfg}{sort_order}, { fallback => sub { shift cmp shift } };
$self
}
sub addpkg {
my ($pkg, $dir) = @_;
return unless $pkg =~ /^[a-zA-Z0-9_-]+$/aa;
my @dirs = ($dir);
find {
postprocess => sub { pop @dirs },
wanted => sub {
my $dest = catfile @dirs, $_;
mkdir $dest if -d;
push @dirs, $_ if -d;
link $_, $dest if -f;
}}, catfile 'pkg', $pkg;
}
sub makepkg {
my ($self, @pkgs) = @_;
mkdir $self->{cfg}{dir};
my $dir = rel2abs tempdir 'workXXXX', DIR => $self->{cfg}{dir};
my $dest = catfile $dir, 'pkg';
mkdir $dest;
@pkgs = grep { exists $self->{cfg}{pkgs}{$_} } @pkgs;
push @pkgs, split /,/, ($self->{cfg}{pkgs}{$_}{deps} // '') for @pkgs;
@pkgs = uniq @pkgs;
addpkg $_, $dest for @pkgs;
write_file catfile ($dir, 'readme.txt'), $self->{cfg}{readme};
my @zip_files = catfile $dir, 'readme.txt';
if ($self->{cfg}{vpk}) {
system $self->{cfg}{vpk} => $dest;
push @zip_files, catfile $dir, "pkg.$self->{cfg}{vpk_extension}"
} else {
find sub { push @zip_files, $File::Find::name if -f }, $dest;
}
zip \@zip_files, catfile($dir, 'pkg.zip'), FilterName => sub { $_ = abs2rel $_, $dir }, -Level => 1;
open my $fh, '<', catfile $dir, 'pkg.zip' or return [500, ['Content-Type' => 'text/plain;charset=utf-8'], ['Error opening pkg.zip']]; ## no critic (RequireBriefOpen)
remove_tree $dir;
[200, ['Content-Type' => 'application/zip', 'Content-Disposition' => 'attachment; filename=pkg.zip'], $fh]
}
sub makelist {
my ($self, $elem, $tree, $lvl, $key) = @_;
my $name = HTML::Element->new('span', class => 'name');
$name->push_content($key);
$elem->push_content($name) if defined $key;
if (ref $tree eq 'ARRAY') {
my $sel = HTML::Element->new('select', name => 'pkg');
my $opt = HTML::Element->new('option', value => '');
$opt->push_content('None');
$sel->push_content($opt);
for my $pkg (sort { $a->{name} cmp $b->{name} } @$tree) {
my $option = HTML::Element->new('option', value => $pkg->{pkg}, $pkg->{default} ? (selected => 'selected') : ());
$option->push_content($pkg->{name});
$sel->push_content($option);
}
$elem->push_content($sel);
} else {
my $ul = HTML::Element->new('ul');
for my $key ($self->{cfg}{sort}->(keys %$tree)) {
my $li = HTML::Element->new('li', class => "level$lvl");
$self->makelist($li, $tree->{$key}, $lvl + 1, $key);
$ul->push_content($li);
}
$elem->push_content($ul);
}
}
sub makeindex {
my ($self) = @_;
my ($pkgs, $tree) = ($self->{cfg}{pkgs}, {});
for (keys %$pkgs) {
my $ref = DiveRef ($tree, split /,/, $pkgs->{$_}{path});
$$ref = [] unless ref $$ref eq 'ARRAY';
push @{$$ref}, {pkg => $_, name => $pkgs->{$_}{name}, default => $pkgs->{$_}{default}};
}
my $html = HTML::TreeBuilder->new_from_file('index.html');
$self->makelist(scalar $html->look_down(id => 'list'), $tree, 1);
my $ret = $html->as_HTML('', ' ');
utf8::encode($ret);
[200, ['Content-Type' => 'text/html;charset=utf-8'], [$ret]]
}
sub call{
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
return $self->makepkg($req->param('pkg')) if $req->path eq '/makepkg';
$self->makeindex;
}
1;
__END__
=encoding utf-8
=head1 NAME
App::Web::VPKBuilder - Mix & match Source engine game mods
=head1 SYNOPSIS
use Plack::Builder;
use App::Web::VPKBuilder;
builder {
enable ...;
enable ...;
App::Web::VPKBuilder->new->to_app
}
=head1 DESCRIPTION
App::Web::VPKBuilder is a simple web service for building Source engine game VPK packages. It presents a list of mods sorted into (sub)categories. The user can choose a mod from each category and will get a VPK containing all of the selected packages...
=head1 CONFIGURATION
APP::Web::VPKBuilder is configured via YAML files in the F<cfg> directory. The recommended layout is to have an F<options.yml> file with the global options, and one file for each source mod (original mod that may be split into more mods).
=head2 Global options
=over
=item readme
A string representing the contents of the readme.txt file included with the package.
=item sort_order
An array of strings representing the sort order of (sub)categories. (sub)categories appear in this order. (sub)categories that are not listed appear in alphabetical order after those listed.
=item dir
A string representing the directory in which the packages are built. Must be on the same filesystem as the package directory (F<pkg/>). Is created if it does not exist (but its parents must exist).
=item vpk
A string representing the program that makes a package out of a folder. Must behave like the vpk program included with Source engine games: that is, when called like C<vpk path/to/folder> it should create a file F<path/to/folder.ext>, where C<ext> is...
=item vpk_extension
The extension of a package. Only useful with the C<vpk> option. Defaults to C<vpk>
( run in 1.898 second using v1.01-cache-2.11-cpan-5837b0d9d2c )