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 foreach (qw(new tmp cur)) {
19 if (!File::Path::mkpath($d) && !-d $d) {
20 die "failed to mkpath($d): $!\n";
23 bless { dir => $dir, files => {}, t => 0, cnt => 0, pid => $$ }, $class;
27 my ($self, $dir) = @_;
28 my @host = split(/\./, hostname);
30 if ($self->{t} != $now) {
39 $f = "$self->{dir}/$dir/$self->{t}.$$"."_$self->{cnt}.$host[0]";
46 my ($self, $strref) = @_;
48 die "already in transaction: $self->{tmp}" if $self->{tmp};
51 $tmp = _fn_in($self, 'tmp');
53 } while (!sysopen($fh, $tmp, O_CREAT|O_EXCL|O_RDWR) && $!{EEXIST});
54 print $fh $$strref or die "write failed: $!";
55 $fh->flush or die "flush failed: $!";
64 my $tmp = delete $self->{tmp} or return;
66 unlink($tmp) or warn "Failed to unlink $tmp: $!";
72 my $fh = $self->{fh} or die "{fh} not open!\n";
73 seek($fh, 0, SEEK_SET) or die "seek(fh) failed: $!";
74 sysseek($fh, 0, SEEK_SET) or die "sysseek(fh) failed: $!";
80 $$ == $self->{pid} or return; # no-op in forked child
83 my $tmp = delete $self->{tmp} or return;
86 $new = _fn_in($self, 'new');
87 } while (!link($tmp, $new) && $!{EEXIST});
88 my @sn = stat($new) or die "stat $new failed: $!";
89 my @st = stat($tmp) or die "stat $tmp failed: $!";
90 if ($st[0] == $sn[0] && $st[1] == $sn[1]) {
91 unlink($tmp) or warn "Failed to unlink $tmp: $!";
93 warn "stat($new) and stat($tmp) differ";
97 sub DESTROY { commit($_[0]) }