]> Sergey Matveev's repositories - public-inbox.git/blob - script/public-inbox-init
treewide: use *nix-specific dirname regexps
[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 my ($dir) = ($pi_config =~ m!(.*?/)[^/]+\z!);
97 require File::Path;
98 File::Path::mkpath($dir); # will croak on fatal errors
99
100 # first, we grab a flock to prevent simultaneous public-inbox-init
101 # processes from trampling over each other, or exiting with 255 on
102 # O_EXCL failure below.  This gets unlocked automatically on exit:
103 require PublicInbox::Lock;
104 my $lock_obj = { lock_path => "$pi_config.flock" };
105 PublicInbox::Lock::lock_acquire($lock_obj);
106
107 # git-config will operate on this (and rename on success):
108 require File::Temp;
109 my $fh = File::Temp->new(TEMPLATE => 'pi-init-XXXX', DIR => $dir);
110
111 # Now, we grab another lock to use git-config(1) locking, so it won't
112 # wait on the lock, unlike some of our internal flock()-based locks.
113 # This is to prevent direct git-config(1) usage from clobbering our
114 # changes.
115 my $lockfile = "$pi_config.lock";
116 my $lockfh;
117 sysopen($lockfh, $lockfile, O_RDWR|O_CREAT|O_EXCL) or do {
118         warn "could not open config file: $lockfile: $!\n";
119         exit(255);
120 };
121 require PublicInbox::OnDestroy;
122 my $auto_unlink = PublicInbox::OnDestroy->new($$, sub { unlink $lockfile });
123 my ($perm, %seen);
124 if (-e $pi_config) {
125         open(my $oh, '<', $pi_config) or die "unable to read $pi_config: $!\n";
126         my @st = stat($oh);
127         $perm = $st[2];
128         defined $perm or die "(f)stat failed on $pi_config: $!\n";
129         chmod($perm & 07777, $fh) or
130                 die "(f)chmod failed on future $pi_config: $!\n";
131         defined(my $old = do { local $/; <$oh> }) or die "read $pi_config: $!\n";
132         print $fh $old or die "failed to write: $!\n";
133         close $oh or die "failed to close $pi_config: $!\n";
134
135         # yes, this conflict checking is racy if multiple instances of this
136         # script are run by the same $PI_DIR
137         my $cfg = PublicInbox::Config->new;
138         my $conflict;
139         foreach my $addr (@address) {
140                 my $found = $cfg->lookup($addr);
141                 if ($found) {
142                         if ($found->{name} ne $name) {
143                                 print STDERR
144                                         "`$addr' already defined for ",
145                                         "`$found->{name}',\n",
146                                         "does not match intend `$name'\n";
147                                 $conflict = 1;
148                         } else {
149                                 $seen{lc($addr)} = 1;
150                         }
151                 }
152         }
153
154         exit(1) if $conflict;
155
156         my $ibx = $cfg->lookup_name($name);
157         $indexlevel //= $ibx->{indexlevel} if $ibx;
158 }
159 my $pi_config_tmp = $fh->filename;
160 close($fh) or die "failed to close $pi_config_tmp: $!\n";
161
162 my $pfx = "publicinbox.$name";
163 my @x = (qw/git config/, "--file=$pi_config_tmp");
164
165 $inboxdir = PublicInbox::Config::rel2abs_collapsed($inboxdir);
166 die "`\\n' not allowed in `$inboxdir'\n" if index($inboxdir, "\n") >= 0;
167
168 if (-f "$inboxdir/inbox.lock") {
169         if (!defined $version) {
170                 $version = 2;
171         } elsif ($version != 2) {
172                 die "$inboxdir is a -V2 inbox, -V$version specified\n"
173         }
174 } elsif (-d "$inboxdir/objects") {
175         if (!defined $version) {
176                 $version = 1;
177         } elsif ($version != 1) {
178                 die "$inboxdir is a -V1 inbox, -V$version specified\n"
179         }
180 }
181
182 $version = 1 unless defined $version;
183
184 if ($version == 1 && defined $skip_epoch) {
185         die "--skip-epoch is only supported for -V2 inboxes\n";
186 }
187
188 my $ibx = PublicInbox::Inbox->new({
189         inboxdir => $inboxdir,
190         name => $name,
191         version => $version,
192         -primary_address => $address[0],
193         indexlevel => $indexlevel,
194 });
195
196 my $creat_opt = {};
197 if (defined $jobs) {
198         die "--jobs is only supported for -V2 inboxes\n" if $version == 1;
199         die "--jobs=$jobs must be >= 1\n" if $jobs <= 0;
200         $creat_opt->{nproc} = $jobs;
201 }
202
203 require PublicInbox::InboxWritable;
204 $ibx = PublicInbox::InboxWritable->new($ibx, $creat_opt);
205 if ($skip_docdata) {
206         $ibx->{indexlevel} //= 'full'; # ensure init_inbox writes xdb
207         $ibx->{indexlevel} eq 'basic' and
208                 die "--skip-docdata ignored with --indexlevel=basic\n";
209         $ibx->{-skip_docdata} = $skip_docdata;
210 }
211 $ibx->init_inbox(0, $skip_epoch, $skip_artnum);
212
213 # needed for git prior to v2.1.0
214 umask(0077) if defined $perm;
215
216 require PublicInbox::Spawn;
217 PublicInbox::Spawn->import(qw(run_die));
218
219 foreach my $addr (@address) {
220         next if $seen{lc($addr)};
221         run_die([@x, "--add", "$pfx.address", $addr]);
222 }
223 run_die([@x, "$pfx.url", $http_url]);
224 run_die([@x, "$pfx.inboxdir", $inboxdir]);
225
226 if (defined($indexlevel)) {
227         run_die([@x, "$pfx.indexlevel", $indexlevel]);
228 }
229 run_die([@x, "$pfx.newsgroup", $ng]) if $ng ne '';
230
231 for my $kv (@c_extra) {
232         my ($k, $v) = split(/=/, $kv, 2);
233         # git 2.30+ has --fixed-value for idempotent invocations,
234         # but that's too new to depend on in 2021.  Perl quotemeta
235         # seems compatible enough for POSIX ERE which git uses
236         my $re = '^'.quotemeta($v).'$';
237         run_die([@x, qw(--replace-all), "$pfx.$k", $v, $re]);
238 }
239
240 # needed for git prior to v2.1.0
241 if (defined $perm) {
242         chmod($perm & 07777, $pi_config_tmp) or
243                         die "(f)chmod failed on future $pi_config: $!\n";
244 }
245
246 rename $pi_config_tmp, $pi_config or
247         die "failed to rename `$pi_config_tmp' to `$pi_config': $!\n";
248 undef $auto_unlink; # trigger ->DESTROY