← retour aux snippets

écriture atomique de fichier avec fsync

Écrire un fichier de configuration de façon atomique (tmp+rename) en flushant sur disque (fsync) et permissions sûres.


objectif

Écrire un fichier de manière sûre: contenu dans un fichier temporaire, flush sur disque (fsync), puis renommage atomique dans le même répertoire. Permissions maîtrisées et option de verrou (flock).

code minimal

# atomic_write TARGET [MODE] - lit le contenu sur stdin, écrit de façon atomique
atomic_write() {
  local target="$1"; local mode="${2:-0644}"
  [ -n "$target" ] || { echo "target manquant" >&2; return 2; }
  local dir base tmp
  dir="$(cd "$(dirname "$target")" && pwd)"; base="$(basename "$target")"
  umask 077
  tmp="$(mktemp -p "$dir" ".${base}.XXXXXX")" || return 3
  # écrire le contenu
  cat > "$tmp" || { rm -f "$tmp"; return 4; }
  # permissions explicites
  chmod "$mode" "$tmp" || { rm -f "$tmp"; return 5; }
  # fsync du fichier puis du dossier (via Python, portable)
  python3 - "$tmp" "$dir" <<'PY' || { rm -f "$tmp"; exit 6; }
import os, sys
p = sys.argv[1]; d = sys.argv[2]
fd = os.open(p, os.O_RDONLY)
try:
    os.fsync(fd)
finally:
    os.close(fd)
dd = os.open(d, os.O_RDONLY)
try:
    os.fsync(dd)
finally:
    os.close(dd)
PY
  # renommage atomique (même FS)
  mv -f "$tmp" "$target"
}

# usage simple:
printf 'server: data.pm\nport: 443\n' | atomic_write "/etc/data.pm/app.conf" 0640

utilisation

# 1) générer un nginx vhost pour data.pm puis écrire atomiquement
generate_nginx() {
  cat <<'NG'
server {
  listen 443 ssl http2;
  server_name data.pm;
  root /srv/data.pm/current;
  location / { try_files $uri $uri/ /index.html; }
}
NG
}
sudo bash -lc 'generate_nginx' | atomic_write "/etc/nginx/sites-available/data.pm.conf" 0644
sudo ln -sfn "/etc/nginx/sites-available/data.pm.conf" "/etc/nginx/sites-enabled/data.pm.conf"

# 2) écrire un .env (api.data.pm) en maîtrisant les droits
{
  printf 'API_URL=%s\n' "https://api.data.pm"
  printf 'APP_ENV=%s\n' "production"
} | atomic_write "/srv/api.data.pm/.env" 0640

# 3) mise à jour d'un JSON de config avec validation préalable
jq '.feature_flags.new_checkout=true' config.json \
  | jq -e . \
  | atomic_write "/srv/data.pm/config/config.json" 0644

# 4) écriture distante atomique (SSH): écrire localement, copier, puis rename côté serveur
tmp_local="$(mktemp)"; trap 'rm -f "$tmp_local"' EXIT
jq -n --arg d "$(date -u +%FT%TZ)" '{deployed_at:$d,site:"data.pm"}' > "$tmp_local"
scp "$tmp_local" deploy@data.pm:/tmp/config.json.new
ssh deploy@data.pm "python3 - <<'PY'
import os, sys
src='/tmp/config.json.new'; dst='/srv/data.pm/config/config.json'
# fsync src
fd=os.open(src, os.O_RDONLY); os.fsync(fd); os.close(fd)
# fsync dir
d=os.path.dirname(dst); dd=os.open(d, os.O_RDONLY); os.fsync(dd); os.close(dd)
# rename atomique
os.replace(src, dst)
PY"

variante(s) utile(s)

# ajouter un verrou global pour éviter les écritures concurrentes
atomic_write_locked() {
  local target="$1"; local mode="${2:-0644}"
  local lock="${target}.lock"
  exec 9>"$lock"
  flock -n 9 || { echo "lock pris: $lock" >&2; return 10; }
  atomic_write "$target" "$mode"
  local rc=$?
  rm -f "$lock" 2>/dev/null || true
  return $rc
}

# écrire depuis un template avec envsubst (data.pm)
envsubst < site.conf.tmpl | sudo bash -lc 'atomic_write "/etc/nginx/sites-available/data.pm.conf" 0644'

# créer le répertoire cible si absent (droits sûrs), puis écrire
install -d -m 0750 /srv/data.pm/config
printf '%s\n' 'maintenance=true' | atomic_write "/srv/data.pm/config/flags" 0640

# macOS: même approche (mktemp, mv). fsync via Python est portable.
printf 'key=value\n' | atomic_write "$HOME/.config/data.pm/app.conf" 0644

# journaliser chaque écriture (append JSONL)
log="/var/log/data.pm/atomic-write.jsonl"
wrap_and_log() {
  local target="$1"; shift
  local ts rc
  ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  cat | atomic_write "$target" "${1:-0644}"; rc=$?
  printf '{"ts":"%s","target":%q,"rc":%d}\n' "$ts" "$target" "$rc" >> "$log"
  return $rc
}
# usage:
printf 'ok\n' | wrap_and_log "/tmp/test.txt" 0644

notes

  • le renommage mv est atomique dans le même répertoire et le même filesystem: créez le fichier temporaire dans dirname(target).
  • fsync du fichier puis du répertoire garantit la durabilité sur disque en cas de crash (évitant un fichier vide ou absent après rename).
  • fixez des permissions explicites (chmod), et laissez un umask 077 le temps de l’écriture pour éviter des fuites.
  • utilisez un verrou (flock) si plusieurs processus peuvent écrire le même fichier.
  • n’appliquez pas chown sans nécessité; si besoin, faites-le côté root après le rename atomique.