#!/bin/sh # Sync image-baked plugins into the /config (datadir) plugins directory on # start. /config is an RWX volume shared across replicas, so plugins baked # into the image are invisible until copied in here -- and this must be safe # when several replicas start (or restart) at the same instant: # - skip entirely once the correct version is already in place, so the # steady state (almost every start) never touches the shared volume; # - install a new/changed version via copy-to-staging + atomic rename, so # no reader (another replica, or this container's own jellyfin process) # ever observes a partially-written plugin directory. A replica that # loses the rename race just discards its own copy -- that's success, # not an error; # - drop stale, differently-versioned copies of the same plugin afterwards # so they don't shadow the current one. Best-effort: another replica may # already be doing, or have finished, the same cleanup. # Deliberately no locking: a lock held by a replica that dies mid-sync would # wedge every future start on this volume, which is worse than the race it # would prevent. set -eu BAKED_DIR=/usr/share/jellyfin/plugins-baked PLUGIN_DIR=/config/plugins STAGING_DIR="$PLUGIN_DIR/.sync-tmp" REPLICA=$(hostname) if [ -d "$BAKED_DIR" ]; then mkdir -p "$PLUGIN_DIR" "$STAGING_DIR" for src in "$BAKED_DIR"/*; do [ -d "$src" ] || continue name=$(basename "$src") # e.g. "LDAP Authentication_24.0.0.0" base=${name%_*} # plugin name without the trailing _ target="$PLUGIN_DIR/$name" if [ ! -d "$target" ]; then # Build the new version privately (keyed by this replica's own # hostname, so concurrent replicas never share a staging path), then # move it into place in one atomic rename. mv -T fails with # "Directory not empty" if another replica's rename already won -- # that's fine, our copy just becomes garbage we discard. staging="$STAGING_DIR/$REPLICA.$name" rm -rf "$staging" cp -a "$src" "$staging" if mv_err=$(mv -T "$staging" "$target" 2>&1); then : elif [ -d "$target" ]; then rm -rf "$staging" else echo "docker-entrypoint: failed to install plugin $name: $mv_err" >&2 exit 1 fi fi # Remove any other version of this plugin so it can't shadow the one # above. Another replica may be racing the same cleanup, or have already # finished it -- an entry that's already gone is success, not an error. for existing in "$PLUGIN_DIR/$base"_*; do [ -e "$existing" ] || continue [ "$existing" = "$target" ] && continue rm -rf "$existing" 2>/dev/null || true done done fi exec ./jellyfin \ --datadir /config \ --cachedir /cache \ --webdir /jellyfin/jellyfin-web \ "$@"