about summary refs log tree commit homepage
path: root/lib/PublicInbox/RepoGitSnapshot.pm
blob: 3d53fa6d9a2ae3823a3f6372c6cc9cbe235f81bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# Copyright (C) 2016 all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>

# 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->{h} || $req->{-repo}->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 = $req->{-repo};

	# support disabling certain snapshots types entirely to twart
	# URL guessing since it could burn server resources.
	return $self->r(404) if $repo->{snapshots_disabled}->{$fmt};

	# strip optional basename (may not exist)
	$ref =~ s/$repo->{snapshot_re}//;

	# don't allow option/command injection, git refs do not start with '-'
	return $self->r(404) if $ref =~ /\A-/;

	my $git = $repo->{git};
	my $tree = '';
	my $last_cb = sub {
		delete $env->{'repobrowse.tree_cb'};
		delete $env->{'qspawn.quiet'};
		my $pfx = "$repo->{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;