1 # Copyright (C) 2016-2020 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4 # Emergency Maildir delivery for MDA
5 package PublicInbox::Emergency;
8 use Fcntl qw(:DEFAULT SEEK_SET);
9 use Sys::Hostname qw(hostname);
10 use IO::Handle; # ->flush, ->autoflush
13 my ($class, $dir) = @_;
15 -d $dir or mkdir($dir) or die "failed to mkdir($dir): $!\n";
16 foreach (qw(new tmp cur)) {
19 -d $d or mkdir($d) or die "failed to mkdir($d): $!\n";
21 bless { dir => $dir, files => {}, t => 0, cnt => 0, pid => $$ }, $class;
25 my ($self, $dir) = @_;
26 my @host = split(/\./, hostname);
28 if ($self->{t} != $now) {
37 $f = "$self->{dir}/$dir/$self->{t}.$$"."_$self->{cnt}.$host[0]";
44 my ($self, $strref) = @_;
46 die "already in transaction: $self->{tmp}" if $self->{tmp};
49 $tmp = _fn_in($self, 'tmp');
51 } while (!sysopen($fh, $tmp, O_CREAT|O_EXCL|O_RDWR) && $!{EEXIST});
52 print $fh $$strref or die "write failed: $!";
53 $fh->flush or die "flush failed: $!";
62 my $tmp = delete $self->{tmp} or return;
64 unlink($tmp) or warn "Failed to unlink $tmp: $!";
70 my $fh = $self->{fh} or die "{fh} not open!\n";
71 seek($fh, 0, SEEK_SET) or die "seek(fh) failed: $!";
72 sysseek($fh, 0, SEEK_SET) or die "sysseek(fh) failed: $!";
78 $$ == $self->{pid} or return; # no-op in forked child
81 my $tmp = delete $self->{tmp} or return;
84 $new = _fn_in($self, 'new');
85 } while (!link($tmp, $new) && $!{EEXIST});
86 my @sn = stat($new) or die "stat $new failed: $!";
87 my @st = stat($tmp) or die "stat $tmp failed: $!";
88 if ($st[0] == $sn[0] && $st[1] == $sn[1]) {
89 unlink($tmp) or warn "Failed to unlink $tmp: $!";
91 warn "stat($new) and stat($tmp) differ";
95 sub DESTROY { commit($_[0]) }