# Copyright (C) 2016 all contributors # License: AGPL-3.0+ # shows the /snapshot/ endpoint for git repositories # Mainly for compatibility reasons with cgit, I'm unsure if # showing this in a repository viewer is a good idea. package PublicInbox::RepoGitSnapshot; use strict; use warnings; use base qw(PublicInbox::RepoBase); use PublicInbox::Git; use PublicInbox::Qspawn; our $SUFFIX; BEGIN { # as described in git-archive(1), users may add support for # other compression schemes such as xz or bz2 via git-config(1): # git config tar.tar.xz.command "xz -c" # git config tar.tar.bz2.command "bzip2 -c" chomp(my @l = `git archive --list`); $SUFFIX = join('|', map { quotemeta $_ } @l); } # Not using standard mime types since the compressed tarballs are # special or do not match my /etc/mime.types. Choose what gitweb # and cgit agree on for compatibility. our %FMT_TYPES = ( 'tar' => 'application/x-tar', 'tar.bz2' => 'application/x-bzip2', 'tar.gz' => 'application/x-gzip', 'tar.xz' => 'application/x-xz', 'tgz' => 'application/x-gzip', 'zip' => 'application/x-zip', ); sub call_git_snapshot ($$) { # invoked by PublicInbox::RepoBase::call my ($self, $req) = @_; my $ref = $req->{-tip}; my $orig_fn = $ref; # just in case git changes refname rules, don't allow wonky filenames # to break the Content-Disposition header, either. return $self->r(404) if $orig_fn =~ /["\s]/s; return $self->r(404) unless ($ref =~ s/\.($SUFFIX)\z//o); my $fmt = $1; my $env = $req->{env}; my $repo_info = $req->{repo_info}; # support disabling certain snapshots types entirely to twart # URL guessing since it could burn server resources. return $self->r(404) if $repo_info->{snapshots_disabled}->{$fmt}; # strip optional basename (may not exist) $ref =~ s/$repo_info->{snapshot_re}//; # don't allow option/command injection, git refs do not start with '-' return $self->r(404) if $ref =~ /\A-/; my $git = $repo_info->{git}; my $tree = ''; my $last_cb = sub { delete $env->{'repobrowse.tree_cb'}; delete $env->{'qspawn.quiet'}; my $pfx = "$repo_info->{snapshot_pfx}-$ref/"; my $cmd = $git->cmd('archive', "--prefix=$pfx", "--format=$fmt", $tree); my $rdr = { 2 => $git->err_begin }; my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); $qsp->psgi_return($env, undef, sub { my $r = $_[0]; return $self->r(500) unless $r; [ 200, [ 'Content-Type', $FMT_TYPES{$fmt} || 'application/octet-stream', 'Content-Disposition', qq(inline; filename="$orig_fn"), 'ETag', qq("$tree") ] ]; }); }; my $cmd = $git->cmd(qw(rev-parse --verify --revs-only)); # try prefixing "v" or "V" for tag names to get the tree my @refs = ("V$ref", "v$ref", $ref); $env->{'qspawn.quiet'} = 1; my $tree_cb = $env->{'repobrowse.tree_cb'} = sub { my ($ref) = @_; if (defined $ref) { $tree = $$ref; chomp $tree; } return $last_cb->() if $tree ne ''; unless (scalar(@refs)) { my $res = delete $env->{'qspawn.response'}; return $res->($self->r(404)); } my $rdr = { 2 => $git->err_begin }; my $r = pop @refs; my $qsp = PublicInbox::Qspawn->new([@$cmd, $r], undef, $rdr); $qsp->psgi_qx($env, undef, $env->{'repobrowse.tree_cb'}); }; sub { $env->{'qspawn.response'} = $_[0]; # kick off the "loop" foreach @refs $tree_cb->(undef); } } 1;