|
| 1 | +#!/bin/sh |
| 2 | +# Run one file operation and publish its result atomically. |
| 3 | +# |
| 4 | +# flux-op [--mkdir] <staging> <destination> -- <command> [args...] |
| 5 | +# |
| 6 | +# The command writes into <staging>, never into <destination>. Only if it |
| 7 | +# succeeds is the result moved into place. A command that fails, a container |
| 8 | +# that is killed, and a node that loses power all leave <destination> exactly as |
| 9 | +# it was, with the incomplete work parked under a name the startup sweep |
| 10 | +# recognises. |
| 11 | +# |
| 12 | +# This lives in the image rather than in the caller so that one container does |
| 13 | +# the work AND the publish: the "did it succeed" and "put it in place" decisions |
| 14 | +# cannot drift apart, and a second container spawn is not needed per operation. |
| 15 | +# |
| 16 | +# Operands arrive as positional parameters and are never interpolated into a |
| 17 | +# command string - "$@" runs the command with its argv intact, so there is no |
| 18 | +# shell parsing of anything the caller supplied. |
| 19 | +set -eu |
| 20 | + |
| 21 | +make_staging=0 |
| 22 | +if [ "${1:-}" = "--mkdir" ]; then |
| 23 | + make_staging=1 |
| 24 | + shift |
| 25 | +fi |
| 26 | + |
| 27 | +if [ "$#" -lt 4 ]; then |
| 28 | + echo "flux-op: usage: flux-op [--mkdir] <staging> <destination> -- <command> [args...]" >&2 |
| 29 | + exit 2 |
| 30 | +fi |
| 31 | + |
| 32 | +staging=$1 |
| 33 | +destination=$2 |
| 34 | +shift 2 |
| 35 | + |
| 36 | +if [ "$1" != "--" ]; then |
| 37 | + echo "flux-op: expected -- before the command" >&2 |
| 38 | + exit 2 |
| 39 | +fi |
| 40 | +shift |
| 41 | + |
| 42 | +# Only the commands that need an existing directory to write into (tar -C, for |
| 43 | +# one) ask for this. A file copy must NOT have it: cp -T refuses to overwrite a |
| 44 | +# directory with a non-directory. |
| 45 | +if [ "$make_staging" -eq 1 ]; then |
| 46 | + mkdir -p "$staging" |
| 47 | +fi |
| 48 | + |
| 49 | +"$@" |
| 50 | + |
| 51 | +# A destination that does not exist is one atomic rename and nothing to clean up. |
| 52 | +# |
| 53 | +# Replacing one that DOES exist cannot be a single rename in the general case. |
| 54 | +# rename(2) refuses a non-empty directory as its target, and refuses to replace a |
| 55 | +# file with a directory (or the reverse) at all - so `mv` would have to delete |
| 56 | +# the existing entry first, and a crash in that window loses the destination |
| 57 | +# outright while the replacement sits under a staging name nobody recognises. |
| 58 | +# |
| 59 | +# Moving the old entry aside first avoids the window whatever the two types are. |
| 60 | +# Both renames are atomic, so the worst a crash leaves is the old data under |
| 61 | +# .flux-old-*, which the startup sweep renames back when it finds the |
| 62 | +# destination missing. Uniform rather than branching on type: the branch is |
| 63 | +# where the file-replaced-by-directory case was originally missed. |
| 64 | +if [ ! -e "$destination" ] && [ ! -L "$destination" ]; then |
| 65 | + mv -T "$staging" "$destination" |
| 66 | +else |
| 67 | + staging_base=$(basename "$staging") |
| 68 | + old="$(dirname "$staging")/.flux-old-${staging_base#.flux-op-}" |
| 69 | + mv -T "$destination" "$old" |
| 70 | + mv -T "$staging" "$destination" |
| 71 | + rm -rf "$old" |
| 72 | +fi |
0 commit comments