Make 'nothing archived' a distinct exit (3) and fsync the dir on publish
Two further hardening findings:
- Destroy-on-success was unsafe: the script exited 0 for empty/no-archive
lists too, so the README's &&-gated loop could remove a list that
produced no backup artifact. Give the no-file outcomes a distinct exit
code 3 ("nothing to archive" — not an error). The destroy loop now
removes only on exit 0 (archive written) + a successful member dump,
and leaves exit-3 lists intact unless ALLOW_NO_ARCHIVE=1. Safe by
default: an unexpected "no archive" can no longer authorise a delete.
- Publish wasn't crash-durable: the handoff fsynced file contents but not
the parent directory, so the rename's directory entry could be lost to
a crash after success was reported and the list removed. fsync the
destination directory after os.replace(), before returning success.
Verified with stubbed mailman-web: NONE/EMPTY -> exit 3 (no file),
FILE -> exit 0 (0600, content intact, source temp unlinked), bad
address -> exit 2. sh -n + shellcheck clean, embedded python compiles.
This commit is contained in:
parent
7b7fcde84a
commit
7c9b13051d
2 changed files with 56 additions and 23 deletions
56
README.md
56
README.md
|
|
@ -46,18 +46,23 @@ mm3-hk-export announce@lists.example.com /root/mm3/announce.mbox
|
|||
mm3-hk-export --gzip announce@lists.example.com /root/mm3/announce.mbox.gz
|
||||
```
|
||||
|
||||
## Outcomes
|
||||
## Outcomes and exit codes
|
||||
|
||||
The script never writes a partial or empty file, and exits `0` on all three:
|
||||
The script never writes a partial or empty file. The **exit code** tells a
|
||||
caller exactly what happened, most importantly whether a backup was produced:
|
||||
|
||||
| Situation | HyperKitty state | Result |
|
||||
|---|---|---|
|
||||
| Archive has messages | row with N emails | writes the mbox, prints `wrote N messages to …` |
|
||||
| Archived but empty | row, 0 emails | no file, prints an "empty archive" notice |
|
||||
| No archive at all | no `MailingList` row | no file; consults core `archive_policy` to say whether the list is `never`-archived or simply has nothing archived yet |
|
||||
| Situation | HyperKitty state | Result | Exit |
|
||||
|---|---|---|:--:|
|
||||
| Archive has messages | row with N emails | writes the mbox, prints `wrote N messages to …` | `0` |
|
||||
| Archived but empty | row, 0 emails | no file, prints an "empty archive" notice | `3` |
|
||||
| No archive at all | no `MailingList` row | no file; consults core `archive_policy` to say whether the list is `never`-archived or simply has nothing archived yet | `3` |
|
||||
| Usage / bad address | n/a | usage message | `2` |
|
||||
| Genuine error (e.g. `mailman-web` failing) | n/a | underlying output on stderr | `1` |
|
||||
|
||||
A genuine error (e.g. `mailman-web` failing) exits non-zero with the underlying
|
||||
output on stderr.
|
||||
Exit **`3`** ("nothing to archive") is deliberately distinct from `0` ("archive
|
||||
written"). A genuinely empty or never-archived list is a normal, expected
|
||||
outcome, but it is not a successful backup, so a backup-and-destroy caller
|
||||
can refuse to delete a list whose archive was never captured (see below).
|
||||
|
||||
## In a backup-and-destroy loop
|
||||
|
||||
|
|
@ -65,19 +70,32 @@ output on stderr.
|
|||
mkdir -p /root/mm3
|
||||
for list in list1 list2; do
|
||||
addr="$list@domain.tld"
|
||||
# Only destroy a list once BOTH the archive export and the member dump
|
||||
# succeed. mm3-hk-export exits 0 (writing no file) for a list with no
|
||||
# archive, so &&-gating still lets genuinely-empty lists through — it only
|
||||
# blocks remove on a real failure (mailman-web error, disk full, bad mv).
|
||||
if mm3-hk-export "$addr" "/root/mm3/$list.mbox" \
|
||||
&& mailman-wrapper members "$addr" > "/root/mm3/$list.members"; then
|
||||
mailman-wrapper remove "$addr"
|
||||
else
|
||||
echo "mm3-hk-export: backup FAILED for $addr — leaving list intact" >&2
|
||||
fi
|
||||
|
||||
mm3-hk-export "$addr" "/root/mm3/$list.mbox"
|
||||
case $? in
|
||||
0) ;; # archive written; safe to proceed
|
||||
3) # nothing to archive (empty / no archive):
|
||||
# no backup file exists, so by default do
|
||||
# not destroy. Override only if you mean it.
|
||||
if [ "${ALLOW_NO_ARCHIVE:-0}" != 1 ]; then
|
||||
echo "skip: $addr has no archive to back up; not removing" >&2
|
||||
continue
|
||||
fi ;;
|
||||
*) echo "skip: $addr export failed; not removing" >&2; continue ;;
|
||||
esac
|
||||
|
||||
mailman-wrapper members "$addr" > "/root/mm3/$list.members" \
|
||||
|| { echo "skip: $addr member dump failed; not removing" >&2; continue; }
|
||||
|
||||
mailman-wrapper remove "$addr"
|
||||
done
|
||||
```
|
||||
|
||||
The loop destroys a list only when its archive was actually written
|
||||
(exit `0`) and its member roster was dumped. A list with nothing to archive
|
||||
(exit `3`) is left intact unless you opt in with `ALLOW_NO_ARCHIVE=1`, so a
|
||||
transient or unexpected "no archive" can never silently authorise a delete.
|
||||
|
||||
Export before `remove`, and never `remove` on a failed backup. (HyperKitty's
|
||||
archive lives in a separate database and is not deleted by core's `remove`, but
|
||||
a failed export must still halt the destroy step.)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue