]> Sergey Matveev's repositories - public-inbox.git/blob - script/public-inbox-init
init: support git <2.30 for "-c KEY=VALUE" args
[public-inbox.git] / script / public-inbox-init
1 #!perl -w
2 # Copyright (C) 2014-2021 all contributors <meta@public-inbox.org>
3 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4 use strict;
5 use v5.10.1;
6 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
7 use Fcntl qw(:DEFAULT);
8 my $help = <<EOF; # the following should fit w/o scrolling in 80x24 term:
9 usage: public-inbox-init NAME INBOX_DIR HTTP_URL ADDRESS [ADDRESS..]
10
11   Initialize a public-inbox
12
13 required arguments:
14
15   NAME                the name of the inbox
16   INBOX_DIR           pathname the inbox
17   HTTP_URL            HTTP (or HTTPS) URL
18   ADDRESS             email address(es), may be specified multiple times
19
20 options:
21
22   -V2                 use scalable public-inbox-v2-format(5)
23   -L LEVEL            index level `basic', `medium', or `full' (default: full)
24   --ng NEWSGROUP      set NNTP newsgroup name
25   -c KEY=VALUE        set additional config option(s)
26   --skip-artnum=NUM   NNTP article numbers to skip
27   --skip-epoch=NUM    epochs to skip (-V2 only)
28   -j JOBS             number of indexing jobs (-V2 only), (default: 4)
29
30 See public-inbox-init(1) man page for full documentation.
31 EOF
32
33 require PublicInbox::Admin;
34 PublicInbox::Admin::require_or_die('-base');
35
36 my ($version, $indexlevel, $skip_epoch, $skip_artnum, $jobs, $show_help);
37 my $skip_docdata;
38 my $ng = '';
39 my @c_extra;
40 my %opts = (
41         'V|version=i' => \$version,
42         'L|index-level|indexlevel=s' => \$indexlevel,
43         'S|skip|skip-epoch=i' => \$skip_epoch,
44         'skip-artnum=i' => \$skip_artnum,
45         'j|jobs=i' => \$jobs,
46         'ng|newsgroup=s' => \$ng,
47         'skip-docdata' => \$skip_docdata,
48         'help|h' => \$show_help,
49         'c=s@' => \@c_extra,
50 );
51 my $usage_cb = sub {
52         print STDERR $help;
53         exit 1;
54 };
55 GetOptions(%opts) or $usage_cb->();
56 if ($show_help) { print $help; exit 0 };
57 my $name = shift @ARGV or $usage_cb->();
58 my $inboxdir = shift @ARGV or $usage_cb->();
59 my $http_url = shift @ARGV or $usage_cb->();
60 my (@address) = @ARGV;
61 @address or $usage_cb->();
62
63 @c_extra = map {
64         my ($k, $v) = split(/=/, $_, 2);
65         defined($v) or die "Usage: -c KEY=VALUE\n";
66         $k =~ /\A[a-z]+\z/i or die "$k contains invalid characters\n";
67         $k = lc($k);
68         if ($k eq 'newsgroup') {
69                 die "newsgroup already set ($ng)\n" if $ng ne '';
70                 $ng = $v;
71                 ();
72         } elsif ($k eq 'address') {
73                 push @address, $v; # for conflict checking
74                 ();
75         } elsif ($k =~ /\A(?:inboxdir|mainrepo)\z/) {
76                 die "$k not allowed via -c $_\n"
77         } elsif ($k eq 'indexlevel') {
78                 defined($indexlevel) and
79                         die "indexlevel already set ($indexlevel)\n";
80                 $indexlevel = $v;
81                 ();
82         } else {
83                 $_
84         }
85 } @c_extra;
86
87 PublicInbox::Admin::indexlevel_ok_or_die($indexlevel) if defined $indexlevel;
88
89 $ng =~ m![^A-Za-z0-9/_\.\-\~\@\+\=:]! and
90         die "--newsgroup `$ng' is not valid\n";
91 ($ng =~ m!\A\.! || $ng =~ m!\.\z!) and
92         die "--newsgroup `$ng' must not start or end with `.'\n";
93
94 require PublicInbox::Config;
95 my $pi_config = PublicInbox::Config->default_file;
96 require File::Basename;
97 my $dir = File::Basename::dirname($pi_config);
98 require File::Path;
99 File::Path::mkpath($dir); # will croak on fatal errors
100
101 # first, we grab a flock to prevent simultaneous public-inbox-init
102 # processes from trampling over each other, or exiting with 255 on
103 # O_EXCL failure below.  This gets unlocked automatically on exit:
104 require PublicInbox::Lock;
105 my $lock_obj = { lock_path => "$pi_config.flock" };
106 PublicInbox::Lock::lock_acquire($lock_obj);
107
108 # git-config will operate on this (and rename on success):
109 require File::Temp;
110 my $fh = File::Temp->new(TEMPLATE => 'pi-init-XXXX', DIR => $dir);
111
112 # Now, we grab another lock to use git-config(1) locking, so it won't
113 # wait on the lock, unlike some of our internal flock()-based locks.
114 # This is to prevent direct git-config(1) usage from clobbering our
115 # changes.
116 my $lockfile = "$pi_config.lock";
117 my $lockfh;
118 sysopen($lockfh, $lockfile, O_RDWR|O_CREAT|O_EXCL) or do {
119         warn "could not open config file: $lockfile: $!\n";
120         exit(255);
121 };
122 require PublicInbox::OnDestroy;
123 my $auto_unlink = PublicInbox::OnDestroy->new($$, sub { unlink $lockfile });
124 my ($perm, %seen);
125 if (-e $pi_config) {
126         open(my $oh, '<', $pi_config) or die "unable to read $pi_config: $!\n";
127         my @st = stat($oh);
128         $perm = $st[2];
129         defined $perm or die "(f)stat failed on $pi_config: $!\n";
130         chmod($perm & 07777, $fh) or
131                 die "(f)chmod failed on future $pi_config: $!\n";
132         defined(my $old = do { local $/; <$oh> }) or die "read $pi_config: $!\n";
133         print $fh $old or die "failed to write: $!\n";
134         close $oh or die "failed to close $pi_config: $!\n";
135
136         # yes, this conflict checking is racy if multiple instances of this
137         # script are run by the same $PI_DIR
138         my $cfg = PublicInbox::Config->new;
139         my $conflict;
140         foreach my $addr (@address) {
141                 my $found = $cfg->lookup($addr);
142                 if ($found) {
143                         if ($found->{name} ne $name) {
144                                 print STDERR
145                                         "`$addr' already defined for ",
146                                         "`$found->{name}',\n",
147                                         "does not match intend `$name'\n";
148                                 $conflict = 1;
149                         } else {
150                                 $seen{lc($addr)} = 1;
151                         }
152                 }
153         }
154
155         exit(1) if $conflict;
156
157         my $ibx = $cfg->lookup_name($name);
158         $indexlevel //= $ibx->{indexlevel} if $ibx;
159 }
160 my $pi_config_tmp = $fh->filename;
161 close($fh) or die "failed to close $pi_config_tmp: $!\n";
162
163 my $pfx = "publicinbox.$name";
164 my @x = (qw/git config/, "--file=$pi_config_tmp");
165
166 $inboxdir = PublicInbox::Config::rel2abs_collapsed($inboxdir);
167 die "`\\n' not allowed in `$inboxdir'\n" if index($inboxdir, "\n") >= 0;
168
169 if (-f "$inboxdir/inbox.lock") {
170         if (!defined $version) {
171                 $version = 2;
172         } elsif ($version != 2) {
173                 die "$inboxdir is a -V2 inbox, -V$version specified\n"
174         }
175 } elsif (-d "$inboxdir/objects") {
176         if (!defined $version) {
177                 $version = 1;
178         } elsif ($version != 1) {
179                 die "$inboxdir is a -V1 inbox, -V$version specified\n"
180         }
181 }
182
183 $version = 1 unless defined $version;
184
185 if ($version == 1 && defined $skip_epoch) {
186         die "--skip-epoch is only supported for -V2 inboxes\n";
187 }
188
189 my $ibx = PublicInbox::Inbox->new({
190         inboxdir => $inboxdir,
191         name => $name,
192         version => $version,
193         -primary_address => $address[0],
194         indexlevel => $indexlevel,
195 });
196
197 my $creat_opt = {};
198 if (defined $jobs) {
199         die "--jobs is only supported for -V2 inboxes\n" if $version == 1;
200         die "--jobs=$jobs must be >= 1\n" if $jobs <= 0;
201         $creat_opt->{nproc} = $jobs;
202 }
203
204 require PublicInbox::InboxWritable;
205 $ibx = PublicInbox::InboxWritable->new($ibx, $creat_opt);
206 if ($skip_docdata) {
207         $ibx->{indexlevel} //= 'full'; # ensure init_inbox writes xdb
208         $ibx->{indexlevel} eq 'basic' and
209                 die "--skip-docdata ignored with --indexlevel=basic\n";
210         $ibx->{-skip_docdata} = $skip_docdata;
211 }
212 $ibx->init_inbox(0, $skip_epoch, $skip_artnum);
213
214 # needed for git prior to v2.1.0
215 umask(0077) if defined $perm;
216
217 require PublicInbox::Spawn;
218 PublicInbox::Spawn->import(qw(run_die));
219
220 foreach my $addr (@address) {
221         next if $seen{lc($addr)};
222         run_die([@x, "--add", "$pfx.address", $addr]);
223 }
224 run_die([@x, "$pfx.url", $http_url]);
225 run_die([@x, "$pfx.inboxdir", $inboxdir]);
226
227 if (defined($indexlevel)) {
228         run_die([@x, "$pfx.indexlevel", $indexlevel]);
229 }
230 run_die([@x, "$pfx.newsgroup", $ng]) if $ng ne '';
231
232 for my $kv (@c_extra) {
233         my ($k, $v) = split(/=/, $kv, 2);
234         # git 2.30+ has --fixed-value for idempotent invocations,
235         # but that's too new to depend on in 2021.  Perl quotemeta
236         # seems compatible enough for POSIX ERE which git uses
237         my $re = '^'.quotemeta($v).'$';
238         run_die([@x, qw(--replace-all), "$pfx.$k", $v, $re]);
239 }
240
241 # needed for git prior to v2.1.0
242 if (defined $perm) {
243         chmod($perm & 07777, $pi_config_tmp) or
244                         die "(f)chmod failed on future $pi_config: $!\n";
245 }
246
247 rename $pi_config_tmp, $pi_config or
248         die "failed to rename `$pi_config_tmp' to `$pi_config': $!\n";
249 undef $auto_unlink; # trigger ->DESTROY