Files
operation-bin/migrate_to_network_drive
lemke_h 224c68e90b bin: consistent naming, pixi_pgroup_install, git cleanup
- Commands lose their .sh extension (create_personal_dir,
  gopass_bernina_setup, gopass_bernina_service_setup,
  kerberos_ticket_renew, migrate_to_network_drive, pixi_local_sync,
  pixi_rebuild_local, sync_personal_config); .sh now only marks files
  that are sourced. kerberos_ticket_renew.sh and pixi_local_sync.sh stay
  as links, since they are started at login.
- Sourced files gain .sh (bpy-env, anaconda_env, anaconda_bernina_env,
  mm_bernina); the old names stay as links.
- All references in scripts, the .desktop file and the docs updated;
  cheatsheet.md regenerated via list_bin_commands.
- New pixi_pgroup_install: installs the shared bernina pixi env into a
  pgroup work folder (group-shared, with its own package cache) and
  makes it visible in JupyterHub as bernina-<exp>-<env>.
- Fix '#/bin/bash' first lines (missing '!') in 11 scripts.
- Stop tracking clutter (.nfs placeholders, __pycache__, vim swap file,
  .bak/_bak/~ copies, delme_*, empty 'me'); add .gitignore.
2026-09-25 15:48:11 +02:00

303 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Move a local dotfile/config (file or directory) from this host's LOCAL
# home into PSI's "Partially Shared Home" (~/network_drives), and register
# it in ~/network_drives/home/linux_home_links.yaml so it follows you to
# every other host. Companion to ~/sync_personal_config, which only
# seeds two hardcoded items (.ipython, .condarc) and never auto-edits an
# existing linux_home_links.yaml. This script is general-purpose, diffs
# before overwriting anything already staged on the network drive, and
# (carefully, with your confirmation) inserts the new entry into an
# existing linux_home_links.yaml rather than just printing a snippet.
#
# Background reading: migrate_to_network_drive.md (same directory, companion
# doc) and https://linux.psi.ch/documentation/user-guide/personal_drive/
#
# IMPORTANT LIMITATION (confirmed by hand): ~/network_drives is an SMB
# mount (via gvfs) that does NOT support the atomic rename/mkstemp writes
# git needs for its object store - `git init` or any commit there fails
# with "Operation not supported (95)". This script therefore REFUSES to
# migrate anything containing a .git directory. Git repos (gopass stores
# included) must stay in local home (or a real NFS mount like /sf/bernina -
# see gopass.md, same directory) and sync via `git`/`gopass sync` against a
# real git remote instead.
#
# Usage:
# ./migrate_to_network_drive <path-relative-to-HOME> [link|copy] [dest-name] [from-host]
#
# Examples:
# ./migrate_to_network_drive .npmrc
# ./migrate_to_network_drive .config/htop copy htop-config
# ./migrate_to_network_drive .npmrc link "" saresb-cons-07 # pull the copy from another host first
#
# [from-host] is optional. If the local path doesn't exist on this host at
# all, you'll be offered an scp fetch from another host interactively
# (default: saresb-vcons-01). Pass [from-host] explicitly to always fetch a
# fresh copy from that host and diff it against whatever's here locally,
# e.g. when consolidating several hosts' independent copies of the same
# dotfile onto one canonical version.
#
# IDEMPOTENT RE-REGISTRATION: if the local path is already a symlink/copy
# that matches the network drive exactly (e.g. re-running this to register
# one more per-file entry inside a directory you already split up by hand -
# see the .ssh/* entries in linux_home_links.yaml for the case this was
# added for), the seed-copy step is skipped entirely - no diff prompt, no
# "overwrite?" confirmation, straight to registering the yaml entry.
#
set -euo pipefail
LOCAL_REL="${1:?Usage: $0 <path-relative-to-HOME> [link|copy] [dest-name] [from-host]}"
MODE="${2:-link}"
DEST_NAME="${3:-}"
FROM_HOST="${4:-}"
DEFAULT_SCP_HOST="saresb-vcons-01"
if [[ "$MODE" != "link" && "$MODE" != "copy" ]]; then
echo "ERROR: mode must be 'link' or 'copy', got '$MODE'" >&2
exit 1
fi
SRC="${HOME}/${LOCAL_REL}"
NETWORK_DRIVES="${HOME}/network_drives"
HOME_DRIVE="${NETWORK_DRIVES}/home"
CONFIG_DIR="${HOME_DRIVE}/linux_config"
LINKS_FILE="${HOME_DRIVE}/linux_home_links.yaml"
[[ -f "${LINKS_FILE}" ]] || LINKS_FILE="${HOME_DRIVE}/.linux_home_links.yaml"
log() { echo "==> $*"; }
ask() {
# ask "prompt" -> 0 (yes) or 1 (no)
local reply
read -r -p "$1 [y/N] " reply
[[ "$reply" =~ ^[Yy]$ ]]
}
# --- optionally fetch the source from another host via scp first -----------
# (PSI accounts are the same username everywhere, so plain host:path works;
# the remote path is relative to $HOME on that host too, same as locally)
fetch_from_host() {
local host="$1" tmp
tmp="$(mktemp -d)"
log "fetching ${host}:${LOCAL_REL} via scp ..."
if ! scp -rq "${host}:${LOCAL_REL}" "${tmp}/fetched" 2>&1; then
echo "ERROR: scp from ${host}:${LOCAL_REL} failed" >&2
rm -rf "${tmp}"
exit 1
fi
if [[ -e "${SRC}" ]]; then
log "comparing the ${host} copy against local ${SRC}"
if [[ -d "${SRC}" ]]; then
diff -rq "${SRC}/" "${tmp}/fetched/" || true
else
diff -u "${SRC}" "${tmp}/fetched" || true
fi
if ! ask "Replace local ${SRC} with the copy fetched from ${host}?"; then
log "keeping local ${SRC} - discarding the fetched copy"
rm -rf "${tmp}"
return 0
fi
rm -rf "${SRC}"
else
mkdir -p "$(dirname "${SRC}")"
fi
mv "${tmp}/fetched" "${SRC}"
rmdir "${tmp}" 2>/dev/null || true
log "local ${SRC} now matches ${host}"
}
if [[ -n "${FROM_HOST}" ]]; then
fetch_from_host "${FROM_HOST}"
elif [[ ! -e "${SRC}" ]]; then
log "${SRC} does not exist on this host"
if ask "Fetch it via scp from another host?"; then
read -r -p "Host [${DEFAULT_SCP_HOST}]: " reply_host
fetch_from_host "${reply_host:-${DEFAULT_SCP_HOST}}"
fi
fi
# --- sanity checks ----------------------------------------------------------
if [[ ! -e "${SRC}" ]]; then
echo "ERROR: ${SRC} does not exist (and wasn't fetched from another host)" >&2
exit 1
fi
if [[ -e "${SRC}/.git" || ( -f "${SRC}" && "$(basename "${SRC}")" == ".git" ) ]] \
|| find "${SRC}" -maxdepth 3 -name .git -print -quit 2>/dev/null | grep -q .; then
cat >&2 <<EOF
ERROR: ${SRC} contains a .git directory.
~/network_drives is an SMB mount that cannot host a git repository - git's
object writes fail there with "Operation not supported". This was tested
by hand while setting up the team gopass store; see gopass.md (same
directory), "Limitations & workarounds", for the real fix - a real NFS
mount like /sf/bernina works fine for git, or independent per-host clones
synced via git/gitea.
Refusing to migrate a git repository onto the network drive.
EOF
exit 1
fi
if [[ ! -d "${HOME_DRIVE}" ]]; then
log "network_drives/home not found - running setup-network-drives"
setup-network-drives || true
fi
if [[ ! -d "${HOME_DRIVE}" ]]; then
echo "ERROR: ${HOME_DRIVE} still missing. Run 'setup-network-drives' manually." >&2
exit 1
fi
# --- work out the destination -----------------------------------------------
if [[ -z "${DEST_NAME}" ]]; then
# default: strip a single leading dot, replace remaining '/' with '-'
DEST_NAME="${LOCAL_REL#.}"
DEST_NAME="${DEST_NAME//\//-}"
fi
DEST="${CONFIG_DIR}/${DEST_NAME}"
# depth of LOCAL_REL below $HOME, needed to compute a working relative
# symlink target - see the companion doc for why this matters: the PSI
# link-creation tool does NOT adjust for nesting, so a naive target only
# resolves correctly for items directly under $HOME.
IFS='/' read -ra _parts <<< "${LOCAL_REL}"
DEPTH=$((${#_parts[@]} - 1))
UPDIRS=""
for ((i = 0; i < DEPTH; i++)); do UPDIRS="../${UPDIRS}"; done
LINK_TARGET="${UPDIRS}network_drives/home/linux_config/${DEST_NAME}"
log "local path: ${SRC}"
log "network path: ${DEST}"
log "mode: ${MODE}"
[[ "${MODE}" == "link" ]] && log "symlink target (relative, depth ${DEPTH}): ${LINK_TARGET}"
# --- check whether local and network are already identical -----------------
#
# Common when re-running this to register one more file out of a directory
# you already split up by hand (e.g. adding another per-file entry inside
# .ssh/ - see linux_home_links.yaml), or when SRC is already a symlink that
# resolves straight to DEST. In that case there's nothing to seed or
# confirm - skip straight to yaml registration below.
ALREADY_IN_SYNC=0
if [[ -e "${DEST}" ]]; then
if [[ -d "${SRC}" ]]; then
diff -rq "${SRC}/" "${DEST}/" >/dev/null 2>&1 && ALREADY_IN_SYNC=1
else
cmp -s "${SRC}" "${DEST}" && ALREADY_IN_SYNC=1
fi
fi
mkdir -p "${CONFIG_DIR}"
if [[ "${ALREADY_IN_SYNC}" == "1" ]]; then
log "${DEST} already matches ${SRC} - skipping the copy step"
else
# --- diff before overwriting anything already on the network drive -----
if [[ -e "${DEST}" ]]; then
log "${DEST} already exists - showing differences before touching anything"
if [[ -d "${SRC}" ]]; then
diff -rq "${SRC}/" "${DEST}/" || true
if ask "Show full recursive diff?"; then
diff -ru "${DEST}/" "${SRC}/" || true
fi
else
diff -u "${DEST}" "${SRC}" || true
fi
if ! ask "Overwrite ${DEST} with the contents of ${SRC}?"; then
log "aborted - nothing changed"
exit 0
fi
fi
# --- seed the network drive (copy, not move - original stays put until
# the symlink swap below succeeds, so a failure here can't lose data)
#
# NOTE: no -a/-p (preserve mode/owner) here - tested by hand, the SMB
# mount rejects permission/ownership writes ("Operation not supported")
# even for plain files, not just git's internals. Content copies fine;
# just don't ask it to also preserve perms.
if [[ -d "${SRC}" ]]; then
rsync -rlt --delete --no-perms --no-owner --no-group "${SRC}/" "${DEST}/"
else
cp "${SRC}" "${DEST}"
fi
log "copied to ${DEST}"
fi
# --- for link mode: replace the local copy with a symlink ------------------
if [[ "${MODE}" == "link" ]]; then
if [[ -L "${SRC}" && "$(readlink -f "${SRC}")" == "$(readlink -f "${DEST}")" ]]; then
log "${SRC} is already a symlink resolving to ${DEST} - nothing to swap"
else
if ! ask "Replace local ${SRC} with a symlink to the network copy?"; then
log "left ${SRC} as a plain copy - re-run and confirm this step when ready"
log "(until then this is NOT yet wired up as a link: entry)"
exit 0
fi
rm -rf "${SRC}"
ln -s "${LINK_TARGET}" "${SRC}"
if [[ "$(readlink -f "${SRC}")" != "$(readlink -f "${DEST}")" ]]; then
echo "ERROR: symlink does not resolve to ${DEST} - something is wrong with the depth math, not touching linux_home_links.yaml" >&2
exit 1
fi
log "symlink verified: ${SRC} -> ${LINK_TARGET}"
fi
fi
# --- register the entry in linux_home_links.yaml ----------------------------
SECTION="${MODE}" # 'link' or 'copy'
KEY_LINE=" '${LOCAL_REL}':"
if [[ "${MODE}" == "link" ]]; then
VALUE_LINE=" to: '${LINK_TARGET}'"
else
VALUE_LINE=" from: 'network_drives/home/linux_config/${DEST_NAME}'"
fi
if [[ -f "${LINKS_FILE}" ]] && grep -qF "'${LOCAL_REL}':" "${LINKS_FILE}"; then
log "an entry for '${LOCAL_REL}' already exists in ${LINKS_FILE} - not touching it"
log "verify it matches what was just set up:"
grep -A1 -F "'${LOCAL_REL}':" "${LINKS_FILE}"
elif [[ -f "${LINKS_FILE}" ]] && grep -qE "^${SECTION}:" "${LINKS_FILE}"; then
awk -v key="${KEY_LINE}" -v val="${VALUE_LINE}" -v section="^${SECTION}:" '
{ print }
$0 ~ section && !done { print key; print val; done=1 }
' "${LINKS_FILE}" > "${LINKS_FILE}.new"
echo "--- proposed change to ${LINKS_FILE} ---"
diff -u "${LINKS_FILE}" "${LINKS_FILE}.new" || true
echo "-----------------------------------------"
if ask "Apply this change?"; then
BACKUP="${LINKS_FILE}.bak.$(date +%Y%m%d%H%M%S)"
cp "${LINKS_FILE}" "${BACKUP}"
mv "${LINKS_FILE}.new" "${LINKS_FILE}"
log "updated ${LINKS_FILE} (backup: ${BACKUP})"
else
rm -f "${LINKS_FILE}.new"
log "left ${LINKS_FILE} unchanged. Add manually:"
echo "${SECTION}:"
echo "${KEY_LINE}"
echo "${VALUE_LINE}"
fi
else
log "no '${SECTION}:' section found in ${LINKS_FILE:-<none>} - add manually:"
echo "${SECTION}:"
echo "${KEY_LINE}"
echo "${VALUE_LINE}"
fi
# --- apply now ---------------------------------------------------------------
if ask "Apply now via create-automatic-links-in-home.service?"; then
systemctl --user restart create-automatic-links-in-home.service \
&& log "applied" \
|| log "could not restart the service here - it will run on next login/ssh/become"
fi
log "done."