about summary refs log tree commit homepage
path: root/lib/PublicInbox/LeiExportKw.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/PublicInbox/LeiExportKw.pm')
-rw-r--r--lib/PublicInbox/LeiExportKw.pm180
1 files changed, 180 insertions, 0 deletions
diff --git a/lib/PublicInbox/LeiExportKw.pm b/lib/PublicInbox/LeiExportKw.pm
new file mode 100644
index 00000000..db4f7441
--- /dev/null
+++ b/lib/PublicInbox/LeiExportKw.pm
@@ -0,0 +1,180 @@
+# Copyright (C) 2021 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# front-end for the "lei export-kw" sub-command
+package PublicInbox::LeiExportKw;
+use strict;
+use v5.10.1;
+use parent qw(PublicInbox::IPC PublicInbox::LeiInput);
+use Errno qw(EEXIST ENOENT);
+
+sub export_kw_md { # LeiMailSync->each_src callback
+        my ($oidbin, $id, $self, $mdir) = @_;
+        my $oidhex = unpack('H*', $oidbin);
+        my $sto_kw = $self->{lse}->oid_keywords($oidhex) or return;
+        my $bn = $$id;
+        my ($md_kw, $unknown, @try);
+        if ($bn =~ s/:2,([a-zA-Z]*)\z//) {
+                ($md_kw, $unknown) = PublicInbox::MdirReader::flags2kw($1);
+                @try = qw(cur new);
+        } else {
+                $unknown = [];
+                @try = qw(new cur);
+        }
+        if ($self->{-merge_kw} && $md_kw) { # merging keywords is the default
+                @$sto_kw{keys %$md_kw} = values(%$md_kw);
+        }
+        $bn .= ':2,'.
+                PublicInbox::LeiToMail::kw2suffix([keys %$sto_kw], @$unknown);
+        my $dst = "$mdir/cur/$bn";
+        my @fail;
+        for my $d (@try) {
+                my $src = "$mdir/$d/$$id";
+                next if $src eq $dst;
+
+                # we use link(2) + unlink(2) since rename(2) may
+                # inadvertently clobber if the "uniquefilename" part wasn't
+                # actually unique.
+                if (link($src, $dst)) { # success
+                        # unlink(2) may ENOENT from parallel invocation,
+                        # ignore it, but not other serious errors
+                        if (!unlink($src) and $! != ENOENT) {
+                                $self->{lei}->child_error(1,
+                                                        "E: unlink($src): $!");
+                        }
+                        $self->{lms}->mv_src("maildir:$mdir",
+                                                $oidbin, $id, $bn) or die;
+                        return; # success anyways if link(2) worked
+                }
+                if ($! == ENOENT && !-e $src) { # some other process moved it
+                        $self->{lms}->clear_src("maildir:$mdir", $id);
+                        next;
+                }
+                push @fail, $src if $! != EEXIST;
+        }
+        return unless @fail;
+        # both tries failed
+        my $e = $!;
+        my $orig = '['.join('|', @fail).']';
+        $self->{lei}->child_error(1, "link($orig, $dst) ($oidhex): $e");
+}
+
+# overrides PublicInbox::LeiInput::input_path_url
+sub input_path_url {
+        my ($self, $input, @args) = @_;
+        my $lms = $self->{lms} //= $self->{lse}->lms;
+        $lms->lms_begin;
+        if ($input =~ s/\Amaildir://i) {
+                require PublicInbox::LeiToMail; # kw2suffix
+                $lms->each_src("maildir:$input", \&export_kw_md, $self, $input);
+        }
+        $lms->lms_commit;
+}
+
+sub lei_export_kw {
+        my ($lei, @folders) = @_;
+        my $sto = $lei->_lei_store or return $lei->fail(<<EOM);
+lei/store uninitialized, see lei-import(1)
+EOM
+        my $lse = $sto->search;
+        my $lms = $lse->lms or return $lei->fail(<<EOM);
+lei mail_sync uninitialized, see lei-import(1)
+EOM
+        my $opt = $lei->{opt};
+        my $all = $opt->{all};
+        my @all = $lms->folders;
+        if (defined $all) { # --all=<local|remote>
+                my %x = map { $_ => $_ } split(/,/, $all);
+                my @ok = grep(defined, delete(@x{qw(local remote), ''}));
+                my @no = keys %x;
+                if (@no) {
+                        @no = (join(',', @no));
+                        return $lei->fail(<<EOM);
+--all=@no not accepted (must be `local' and/or `remote')
+EOM
+                }
+                my (%seen, @inc);
+                for my $ok (@ok) {
+                        if ($ok eq 'local') {
+                                @inc = grep(!m!\A[a-z0-9\+]+://!i, @all);
+                        } elsif ($ok eq 'remote') {
+                                @inc = grep(m!\A[a-z0-9\+]+://!i, @all);
+                        } elsif ($ok ne '') {
+                                return $lei->fail("--all=$all not understood");
+                        } else {
+                                @inc = @all;
+                        }
+                        for (@inc) {
+                                push(@folders, $_) unless $seen{$_}++;
+                        }
+                }
+                return $lei->fail(<<EOM) if !@folders;
+no --mail-sync folders known to lei
+EOM
+        } else {
+                my %all = map { $_ => 1 } @all;
+                my @no;
+                for (@folders) {
+                        next if $all{$_}; # ok
+                        if (-d "$_/new" && -d "$_/cur") {
+                                my $d = 'maildir:'.$lei->rel2abs($_);
+                                push(@no, $_) unless $all{$d};
+                                $_ = $d;
+                        } else {
+                                push @no, $_;
+                        }
+                }
+                my $no = join("\n\t", @no);
+                return $lei->fail(<<EOF) if @no;
+No sync information for: $no
+Run `lei ls-mail-sync' to display valid choices
+EOF
+        }
+        my $self = bless { lse => $lse }, __PACKAGE__;
+        $lei->{opt}->{'mail-sync'} = 1; # for prepare_inputs
+        $self->prepare_inputs($lei, \@folders) or return;
+        my $j = $opt->{jobs} // scalar(@{$self->{inputs}}) || 1;
+        if (my @ro = grep(!/\A(?:maildir|imaps?):/, @folders)) {
+                return $lei->fail("cannot export to read-only folders: @ro");
+        }
+        if (my $net = $lei->{net}) {
+                require PublicInbox::NetWriter;
+                bless $net, 'PublicInbox::NetWriter';
+        }
+        undef $lms;
+        my $m = $opt->{mode} // 'merge';
+        if ($m eq 'merge') { # default
+                $self->{-merge_kw} = 1;
+        } elsif ($m eq 'set') {
+        } else {
+                return $lei->fail(<<EOM);
+--mode=$m not supported (`set' or `merge')
+EOM
+        }
+        my $ops = {};
+        $lei->{auth}->op_merge($ops, $self) if $lei->{auth};
+        $self->{-wq_nr_workers} = $j // 1; # locked
+        (my $op_c, $ops) = $lei->workers_start($self, $j, $ops);
+        $lei->{wq1} = $self;
+        $lei->{-err_type} = 'non-fatal';
+        net_merge_all_done($self) unless $lei->{auth};
+        $op_c->op_wait_event($ops); # calls net_merge_all_done if $lei->{auth}
+}
+
+sub _complete_export_kw {
+        my ($lei, @argv) = @_;
+        my $sto = $lei->_lei_store or return;
+        my $lms = $sto->search->lms or return;
+        my $match_cb = $lei->complete_url_prepare(\@argv);
+        map { $match_cb->($_) } $lms->folders;
+}
+
+no warnings 'once';
+
+*ipc_atfork_child = \&PublicInbox::LeiInput::input_only_atfork_child;
+*net_merge_all_done = \&PublicInbox::LeiInput::input_only_net_merge_all_done;
+
+# the following works even when LeiAuth is lazy-loaded
+*net_merge_all = \&PublicInbox::LeiAuth::net_merge_all;
+
+1;