]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/LeiStore.pm
lei: refine help/option parsing, implement "init"
[public-inbox.git] / lib / PublicInbox / LeiStore.pm
1 # Copyright (C) 2020 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 #
4 # Local storage (cache/memo) for lei(1), suitable for personal/private
5 # mail iff on encrypted device/FS.  Based on v2, but only deduplicates
6 # based on git OID.
7 #
8 # for xref3, the following are constant: $eidx_key = '.', $xnum = -1
9 package PublicInbox::LeiStore;
10 use strict;
11 use v5.10.1;
12 use parent qw(PublicInbox::Lock);
13 use PublicInbox::SearchIdx qw(crlf_adjust);
14 use PublicInbox::ExtSearchIdx;
15 use PublicInbox::Import;
16 use PublicInbox::InboxWritable;
17 use PublicInbox::V2Writable;
18 use PublicInbox::ContentHash qw(content_hash);
19 use PublicInbox::MID qw(mids);
20 use PublicInbox::LeiSearch;
21
22 sub new {
23         my (undef, $dir, $opt) = @_;
24         my $eidx = PublicInbox::ExtSearchIdx->new($dir, $opt);
25         my $self = bless { priv_eidx => $eidx }, __PACKAGE__;
26         if ($opt->{creat}) {
27                 PublicInbox::SearchIdx::load_xapian_writable();
28                 eidx_init($self);
29         }
30         $self;
31 }
32
33 sub git { $_[0]->{priv_eidx}->git } # read-only
34
35 sub packing_factor { $PublicInbox::V2Writable::PACKING_FACTOR }
36
37 sub rotate_bytes {
38         $_[0]->{rotate_bytes} // ((1024 * 1024 * 1024) / $_[0]->packing_factor)
39 }
40
41 sub git_pfx { "$_[0]->{priv_eidx}->{topdir}/local" };
42
43 sub git_epoch_max  {
44         my ($self) = @_;
45         my $pfx = $self->git_pfx;
46         my $max = 0;
47         return $max unless -d $pfx ;
48         opendir my $dh, $pfx or die "opendir $pfx: $!\n";
49         while (defined(my $git_dir = readdir($dh))) {
50                 $git_dir =~ m!\A([0-9]+)\.git\z! or next;
51                 $max = $1 + 0 if $1 > $max;
52         }
53         $max;
54 }
55
56 sub importer {
57         my ($self) = @_;
58         my $max;
59         my $im = $self->{im};
60         if ($im) {
61                 return $im if $im->{bytes_added} < $self->rotate_bytes;
62
63                 delete $self->{im};
64                 $im->done;
65                 undef $im;
66                 $self->checkpoint;
67                 $max = $self->git_epoch_max + 1;
68         }
69         my $pfx = $self->git_pfx;
70         $max //= $self->git_epoch_max;
71         while (1) {
72                 my $latest = "$pfx/$max.git";
73                 my $old = -e $latest;
74                 my $git = PublicInbox::Git->new($latest);
75                 PublicInbox::Import::init_bare({ git => $git });
76                 $git->qx(qw(config core.sharedRepository 0600)) if !$old;
77                 my $packed_bytes = $git->packed_bytes;
78                 my $unpacked_bytes = $packed_bytes / $self->packing_factor;
79                 if ($unpacked_bytes >= $self->rotate_bytes) {
80                         $max++;
81                         next;
82                 }
83                 chomp(my $i = $git->qx(qw(var GIT_COMMITTER_IDENT)));
84                 die "$git->{git_dir} GIT_COMMITTER_IDENT failed\n" if $?;
85                 my ($n, $e) = ($i =~ /\A(.+) <([^>]+)> [0-9]+ [-\+]?[0-9]+$/g)
86                         or die "could not extract name/email from `$i'\n";
87                 $self->{im} = $im = PublicInbox::Import->new($git, $n, $e);
88                 $im->{bytes_added} = int($packed_bytes / $self->packing_factor);
89                 $im->{lock_path} = undef;
90                 $im->{path_type} = 'v2';
91                 return $im;
92         }
93 }
94
95 sub search {
96         PublicInbox::LeiSearch->new($_[0]->{priv_eidx}->{topdir});
97 }
98
99 sub eidx_init {
100         my ($self) = @_;
101         my $eidx = $self->{priv_eidx};
102         $eidx->idx_init({-private => 1});
103         $eidx;
104 }
105
106 sub _docids_for ($$) {
107         my ($self, $eml) = @_;
108         my %docids;
109         my $chash = content_hash($eml);
110         my $eidx = eidx_init($self);
111         my $oidx = $eidx->{oidx};
112         my $im = $self->{im};
113         for my $mid (@{mids($eml)}) {
114                 my ($id, $prev);
115                 while (my $cur = $oidx->next_by_mid($mid, \$id, \$prev)) {
116                         my $oid = $cur->{blob};
117                         my $docid = $cur->{num};
118                         my $bref = $im ? $im->cat_blob($oid) : undef;
119                         $bref //= $eidx->git->cat_file($oid) // do {
120                                 warn "W: $oid (#$docid) <$mid> not found\n";
121                                 next;
122                         };
123                         local $self->{current_info} = $oid;
124                         my $x = PublicInbox::Eml->new($bref);
125                         $docids{$docid} = $docid if content_hash($x) eq $chash;
126                 }
127         }
128         sort { $a <=> $b } values %docids;
129 }
130
131 sub set_eml_keywords {
132         my ($self, $eml, @kw) = @_;
133         my $eidx = eidx_init($self);
134         my @docids = _docids_for($self, $eml);
135         for my $docid (@docids) {
136                 $eidx->idx_shard($docid)->shard_set_keywords($docid, @kw);
137         }
138         \@docids;
139 }
140
141 sub add_eml_keywords {
142         my ($self, $eml, @kw) = @_;
143         my $eidx = eidx_init($self);
144         my @docids = _docids_for($self, $eml);
145         for my $docid (@docids) {
146                 $eidx->idx_shard($docid)->shard_add_keywords($docid, @kw);
147         }
148         \@docids;
149 }
150
151 sub remove_eml_keywords {
152         my ($self, $eml, @kw) = @_;
153         my $eidx = eidx_init($self);
154         my @docids = _docids_for($self, $eml);
155         for my $docid (@docids) {
156                 $eidx->idx_shard($docid)->shard_remove_keywords($docid, @kw);
157         }
158         \@docids;
159 }
160
161 sub add_eml {
162         my ($self, $eml) = @_;
163         my $eidx = eidx_init($self);
164         my $oidx = $eidx->{oidx};
165         my $smsg = bless { -oidx => $oidx }, 'PublicInbox::Smsg';
166         my $im = $self->importer;
167         $im->add($eml, undef, $smsg) or return; # duplicate returns undef
168         my $msgref = delete $smsg->{-raw_email};
169         $smsg->{bytes} = $smsg->{raw_bytes} + crlf_adjust($$msgref);
170
171         local $self->{current_info} = $smsg->{blob};
172         if (my @docids = _docids_for($self, $eml)) {
173                 for my $docid (@docids) {
174                         my $idx = $eidx->idx_shard($docid);
175                         $oidx->add_xref3($docid, -1, $smsg->{blob}, '.');
176                         $idx->shard_add_eidx_info($docid, '.', $eml); # List-Id
177                 }
178         } else {
179                 $smsg->{num} = $oidx->adj_counter('eidx_docid', '+');
180                 $oidx->add_overview($eml, $smsg);
181                 $oidx->add_xref3($smsg->{num}, -1, $smsg->{blob}, '.');
182                 my $idx = $eidx->idx_shard($smsg->{num});
183                 $idx->index_raw($msgref, $eml, $smsg);
184         }
185         $smsg->{blob}
186 }
187
188 sub done {
189         my ($self) = @_;
190         my $err = '';
191         if (my $im = delete($self->{im})) {
192                 eval { $im->done };
193                 if ($@) {
194                         $err .= "import done: $@\n";
195                         warn $err;
196                 }
197         }
198         $self->{priv_eidx}->done;
199         die $err if $err;
200 }
201
202 1;