diff --git a/src/Hydra/lib/Hydra/Base/Controller/Nix.pm b/src/Hydra/lib/Hydra/Base/Controller/Nix.pm new file mode 100644 index 00000000..9355f1cc --- /dev/null +++ b/src/Hydra/lib/Hydra/Base/Controller/Nix.pm @@ -0,0 +1,27 @@ +package Hydra::Base::Controller::Nix; + +use strict; +use warnings; +use parent 'Catalyst::Controller'; +use Hydra::Helper::Nix; +use Hydra::Helper::CatalystUtils; + + +sub closure : Chained('nix') PathPart { + my ($self, $c) = @_; + $c->stash->{current_view} = 'Hydra::View::NixClosure'; + + # !!! quick hack; this is to make HEAD requests return the right + # MIME type. This is set in the view as well, but the view isn't + # called for HEAD requests. There should be a cleaner solution... + $c->response->content_type('application/x-nix-export'); +} + + +sub manifest : Chained('nix') PathPart Args(0) { + my ($self, $c) = @_; + $c->stash->{current_view} = 'Hydra::View::NixManifest'; +} + + +1; diff --git a/src/Hydra/lib/Hydra/Controller/Build.pm b/src/Hydra/lib/Hydra/Controller/Build.pm index ae8077b6..a5d21b3f 100644 --- a/src/Hydra/lib/Hydra/Controller/Build.pm +++ b/src/Hydra/lib/Hydra/Controller/Build.pm @@ -2,7 +2,7 @@ package Hydra::Controller::Build; use strict; use warnings; -use parent 'Catalyst::Controller'; +use base 'Hydra::Base::Controller::Nix'; use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; @@ -18,12 +18,9 @@ sub build : Chained('/') PathPart CaptureArgs(1) { $c->stash->{id} = $id; $c->stash->{build} = getBuild($c, $id); - - if (!defined $c->stash->{build}) { - error($c, "Build with ID $id doesn't exist."); - $c->response->status(404); - return; - } + + notFound($c, "Build with ID $id doesn't exist.") + if !defined $c->stash->{build}; $c->stash->{curProject} = $c->stash->{build}->project; } @@ -49,7 +46,7 @@ sub view_nixlog : Chained('build') PathPart('nixlog') Args(1) { my ($self, $c, $stepnr) = @_; my $step = $c->stash->{build}->buildsteps->find({stepnr => $stepnr}); - return error($c, "Build doesn't have a build step $stepnr.") if !defined $step; + notFound($c, "Build doesn't have a build step $stepnr.") if !defined $step; $c->stash->{template} = 'log.tt'; $c->stash->{step} = $step; @@ -62,7 +59,7 @@ sub view_nixlog : Chained('build') PathPart('nixlog') Args(1) { sub view_log : Chained('build') PathPart('log') Args(0) { my ($self, $c) = @_; - return error($c, "Build didn't produce a log.") if !defined $c->stash->{build}->resultInfo->logfile; + error($c, "Build didn't produce a log.") if !defined $c->stash->{build}->resultInfo->logfile; $c->stash->{template} = 'log.tt'; @@ -89,13 +86,13 @@ sub download : Chained('build') PathPart('download') { my ($self, $c, $productnr, $filename, @path) = @_; my $product = $c->stash->{build}->buildproducts->find({productnr => $productnr}); - return error($c, "Build doesn't have a product $productnr.") if !defined $product; + notFound($c, "Build doesn't have a product $productnr.") if !defined $product; - return error($c, "Product " . $product->path . " has disappeared.") unless -e $product->path; + error($c, "Product " . $product->path . " has disappeared.") unless -e $product->path; # Security paranoia. foreach my $elem (@path) { - return error($c, "Invalid filename $elem.") if $elem !~ /^$pathCompRE$/; + error($c, "Invalid filename $elem.") if $elem !~ /^$pathCompRE$/; } my $path = $product->path; @@ -108,12 +105,26 @@ sub download : Chained('build') PathPart('download') { $path = "$path/index.html" if -d $path && -e "$path/index.html"; - if (!-e $path) { - return error($c, "File $path does not exist."); - } + notFound($c, "File $path does not exist.") if !-e $path; $c->serve_static_file($path); } +sub nix : Chained('build') PathPart('nix') CaptureArgs(0) { + my ($self, $c) = @_; + + my $build = $c->stash->{build}; + + error($c, "Build cannot be downloaded as a closure or Nix package.") + if !$build->buildproducts->find({type => "nix-build"}); + + error($c, "Path " . $build->outpath . " is no longer available.") + unless isValidPath($build->outpath); + + $c->stash->{name} = $build->nixname; + $c->stash->{storePaths} = [$build->outpath]; +} + + 1; diff --git a/src/Hydra/lib/Hydra/Controller/Root.pm b/src/Hydra/lib/Hydra/Controller/Root.pm index 3ad52585..f46c4a17 100644 --- a/src/Hydra/lib/Hydra/Controller/Root.pm +++ b/src/Hydra/lib/Hydra/Controller/Root.pm @@ -2,7 +2,7 @@ package Hydra::Controller::Root; use strict; use warnings; -use parent 'Catalyst::Controller'; +use base 'Catalyst::Controller'; use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; @@ -152,7 +152,7 @@ sub releasesets :Local { $c->stash->{template} = 'releasesets.tt'; my $project = $c->model('DB::Projects')->find($projectName); - return error($c, "Project $projectName doesn't exist.") if !defined $project; + notFound($c, "Project $projectName doesn't exist.") if !defined $project; $c->stash->{curProject} = $project; $c->stash->{releaseSets} = [$project->releasesets->all]; @@ -225,7 +225,7 @@ sub releases :Local { return requireLogin($c) if !$c->user_exists; - return error($c, "Only the project owner or the administrator can perform this operation.") + error($c, "Only the project owner or the administrator can perform this operation.") unless $c->check_user_roles('admin') || $c->user->username eq $project->owner->username; if ($subcommand eq "edit") { @@ -247,7 +247,7 @@ sub releases :Local { return $c->res->redirect($c->uri_for("/releasesets", $projectName)); } - else { return error($c, "Unknown subcommand."); } + else { error($c, "Unknown subcommand."); } } $c->stash->{template} = 'releases.tt'; @@ -267,24 +267,19 @@ sub create_releaseset :Local { return requireLogin($c) if !$c->user_exists; - return error($c, "Only the project owner or the administrator can perform this operation.") + error($c, "Only the project owner or the administrator can perform this operation.") unless $c->check_user_roles('admin') || $c->user->username eq $project->owner->username; if (defined $subcommand && $subcommand eq "submit") { - eval { - my $releaseSetName = $c->request->params->{name}; - $c->model('DB')->schema->txn_do(sub { - # Note: $releaseSetName is validated in updateProject, - # which will abort the transaction if the name isn't - # valid. - my $releaseSet = $project->releasesets->create({name => $releaseSetName}); - updateReleaseSet($c, $releaseSet); - return $c->res->redirect($c->uri_for("/releases", $projectName, $releaseSet->name)); - }); - }; - if ($@) { - return error($c, $@); - } + my $releaseSetName = $c->request->params->{name}; + $c->model('DB')->schema->txn_do(sub { + # Note: $releaseSetName is validated in updateProject, + # which will abort the transaction if the name isn't + # valid. + my $releaseSet = $project->releasesets->create({name => $releaseSetName}); + updateReleaseSet($c, $releaseSet); + return $c->res->redirect($c->uri_for("/releases", $projectName, $releaseSet->name)); + }); } $c->stash->{template} = 'edit-releaseset.tt'; @@ -301,7 +296,7 @@ sub release :Local { if ($releaseId eq "latest") { # Redirect to the latest successful release. my $latest = getLatestSuccessfulRelease($project, $primaryJob, $jobs); - return error($c, "This release set has no successful releases yet.") if !defined $latest; + error($c, "This release set has no successful releases yet.") if !defined $latest; return $c->res->redirect($c->uri_for("/release", $projectName, $releaseSetName, $latest->id)); } @@ -309,7 +304,7 @@ sub release :Local { # build, but who cares? my $primaryBuild = $project->builds->find($releaseId, { join => 'resultInfo', '+select' => ["resultInfo.releasename"], '+as' => ["releasename"] }); - return error($c, "Release $releaseId doesn't exist.") if !defined $primaryBuild; + error($c, "Release $releaseId doesn't exist.") if !defined $primaryBuild; $c->stash->{release} = getRelease($primaryBuild, $jobs); } @@ -447,7 +442,7 @@ sub project :Local { $c->stash->{template} = 'project.tt'; my $project = $c->model('DB::Projects')->find($projectName); - return error($c, "Project $projectName doesn't exist.") if !defined $project; + notFound($c, "Project $projectName doesn't exist.") if !defined $project; my $isPosted = $c->request->method eq "POST"; @@ -468,7 +463,7 @@ sub project :Local { return requireLogin($c) if !$c->user_exists; - return error($c, "Only the project owner or the administrator can perform this operation.") + error($c, "Only the project owner or the administrator can perform this operation.") unless $c->check_user_roles('admin') || $c->user->username eq $project->owner->username; if ($subcommand eq "edit") { @@ -490,7 +485,7 @@ sub project :Local { } else { - return error($c, "Unknown subcommand $subcommand."); + error($c, "Unknown subcommand $subcommand."); } } @@ -506,25 +501,20 @@ sub createproject :Local { return requireLogin($c) if !$c->user_exists; - return error($c, "Only administrators can create projects.") + error($c, "Only administrators can create projects.") unless $c->check_user_roles('admin'); if (defined $subcommand && $subcommand eq "submit") { - eval { - my $projectName = trim $c->request->params->{name}; - $c->model('DB')->schema->txn_do(sub { - # Note: $projectName is validated in updateProject, - # which will abort the transaction if the name isn't - # valid. Idem for the owner. - my $project = $c->model('DB::Projects')->create( - {name => $projectName, displayname => "", owner => trim $c->request->params->{owner}}); - updateProject($c, $project); - }); - return $c->res->redirect($c->uri_for("/project", $projectName)); - }; - if ($@) { - return error($c, $@); - } + my $projectName = trim $c->request->params->{name}; + $c->model('DB')->schema->txn_do(sub { + # Note: $projectName is validated in updateProject, + # which will abort the transaction if the name isn't + # valid. Idem for the owner. + my $project = $c->model('DB::Projects')->create( + {name => $projectName, displayname => "", owner => trim $c->request->params->{owner}}); + updateProject($c, $project); + }); + return $c->res->redirect($c->uri_for("/project", $projectName)); } $c->stash->{template} = 'project.tt'; @@ -538,7 +528,7 @@ sub job :Local { $c->stash->{template} = 'job.tt'; my $project = $c->model('DB::Projects')->find($projectName); - return error($c, "Project $projectName doesn't exist.") if !defined $project; + notFound($c, "Project $projectName doesn't exist.") if !defined $project; $c->stash->{curProject} = $project; $c->stash->{jobName} = $jobName; @@ -550,44 +540,7 @@ sub job :Local { sub default :Path { my ($self, $c) = @_; - error($c, "Page not found."); - $c->response->status(404); -} - - -sub closure :Local { - my ($self, $c, $buildId) = @_; - - my $build = getBuild($c, $buildId); - return error($c, "Build $buildId doesn't exist.") if !defined $build; - - return error($c, "Build $buildId cannot be downloaded as a closure.") - if !$build->buildproducts->find({type => "nix-build"}); - - return error($c, "Path " . $build->outpath . " is no longer available.") unless isValidPath($build->outpath); - - $c->stash->{current_view} = 'Hydra::View::NixClosure'; - $c->stash->{storePath} = $build->outpath; - $c->stash->{name} = $build->nixname; - - # !!! quick hack; this is to make HEAD requests return the right - # MIME type. This is set in the view as well, but the view isn't - # called for HEAD requests. There should be a cleaner solution... - $c->response->content_type('application/x-nix-export'); - $c->response->header('Content-Disposition' => 'attachment; filename=' . $c->stash->{name} . '.closure.gz'); -} - - -sub manifest :Local { - my ($self, $c, $buildId) = @_; - - my $build = getBuild($c, $buildId); - return error($c, "Build with ID $buildId doesn't exist.") if !defined $build; - - return error($c, "Path " . $build->outpath . " is no longer available.") unless isValidPath($build->outpath); - - $c->stash->{current_view} = 'Hydra::View::NixManifest'; - $c->stash->{storePath} = $build->outpath; + notFound($c, "Page not found."); } @@ -595,12 +548,12 @@ sub nixpkg :Local { my ($self, $c, $buildId) = @_; my $build = getBuild($c, $buildId); - return error($c, "Build $buildId doesn't exist.") if !defined $build; + notFound($c, "Build $buildId doesn't exist.") if !defined $build; - return error($c, "Build $buildId cannot be downloaded as a Nix package.") + error($c, "Build $buildId cannot be downloaded as a Nix package.") if !$build->buildproducts->find({type => "nix-build"}); - return error($c, "Path " . $build->outpath . " is no longer available.") unless isValidPath($build->outpath); + error($c, "Path " . $build->outpath . " is no longer available.") unless isValidPath($build->outpath); $c->stash->{current_view} = 'Hydra::View::NixPkg'; $c->stash->{build} = $build; @@ -614,7 +567,7 @@ sub nar :Local { my $path .= "/" . join("/", @rest); - return error($c, "Path " . $path . " is no longer available.") unless isValidPath($path); + error($c, "Path " . $path . " is no longer available.") unless isValidPath($path); $c->stash->{current_view} = 'Hydra::View::NixNAR'; $c->stash->{storePath} = $path; diff --git a/src/Hydra/lib/Hydra/Helper/CatalystUtils.pm b/src/Hydra/lib/Hydra/Helper/CatalystUtils.pm index 9fc71389..66577520 100644 --- a/src/Hydra/lib/Hydra/Helper/CatalystUtils.pm +++ b/src/Hydra/lib/Hydra/Helper/CatalystUtils.pm @@ -4,7 +4,7 @@ use strict; use Exporter; our @ISA = qw(Exporter); -our @EXPORT = qw(getBuild error); +our @EXPORT = qw(getBuild error notFound); sub getBuild { @@ -21,4 +21,11 @@ sub error { } +sub notFound { + my ($c, $msg) = @_; + $c->response->status(404); + error($c, $msg); +} + + 1; diff --git a/src/Hydra/lib/Hydra/View/NixClosure.pm b/src/Hydra/lib/Hydra/View/NixClosure.pm index 3d69d7ee..ec820b6d 100644 --- a/src/Hydra/lib/Hydra/View/NixClosure.pm +++ b/src/Hydra/lib/Hydra/View/NixClosure.pm @@ -5,14 +5,13 @@ use base qw/Catalyst::View/; use IO::Pipe; sub process { - my ( $self, $c ) = @_; + my ($self, $c) = @_; $c->response->content_type('application/x-nix-export'); - $c->response->header('Content-Disposition' => 'attachment; filename=' . $c->stash->{name} . '.closure.gz'); - my $storePath = $c->stash->{storePath}; + my @storePaths = @{$c->stash->{storePaths}}; - open(OUTPUT, "nix-store --export `nix-store -qR $storePath` | gzip |"); + open(OUTPUT, "nix-store --export `nix-store -qR @storePaths` | gzip |"); my $fh = new IO::Handle; $fh->fdopen(fileno(OUTPUT), "r") or die; diff --git a/src/Hydra/lib/Hydra/View/NixManifest.pm b/src/Hydra/lib/Hydra/View/NixManifest.pm index 2d9b555a..cb45c197 100644 --- a/src/Hydra/lib/Hydra/View/NixManifest.pm +++ b/src/Hydra/lib/Hydra/View/NixManifest.pm @@ -16,12 +16,12 @@ sub captureStdoutStderr { sub process { my ($self, $c) = @_; - my $storePath = $c->stash->{storePath}; + my @storePaths = @{$c->stash->{storePaths}}; $c->response->content_type('text/x-nix-manifest'); - my @paths = split '\n', `nix-store --query --requisites $storePath` - or die "cannot query dependencies of `$storePath': $?"; + my @paths = split '\n', `nix-store --query --requisites @storePaths` + or die "cannot query dependencies of path(s) @storePaths: $?"; my $manifest = "version {\n" . diff --git a/src/Hydra/root/product-list.tt b/src/Hydra/root/product-list.tt index 8331c69f..eda24140 100644 --- a/src/Hydra/root/product-list.tt +++ b/src/Hydra/root/product-list.tt @@ -43,7 +43,10 @@
  • - + [% filename = "${build.nixname}.closure.gz" %] + [% uri = c.uri_for('/build' build.id 'nix' 'closure' filename ) %] + + Source Nix closure of path [% product.path %] @@ -54,11 +57,11 @@ all its dependencies can be unpacked into your local Nix store by doing:

    -
    $ gunzip < [% HTML.escape(build.nixname) %].closure.gz | nix-store --import
    +
    $ gunzip < [% filename %] | nix-store --import
    or to download and unpack in one command: -
    $ curl [% c.uri_for('/closure' build.id) %] | gunzip | nix-store --import
    +
    $ curl [% uri %] | gunzip | nix-store --import

    The package can then be found in the path [% product.path %]. You’ll probably also want to do