#!/bin/bash # ============================================================================================== # ================================= Ramdisk Setup ============================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Creates the tmpfs ramdisk, SSD fallback directory, transcode symlink, and # pre-creates transcoding-temp on the ramdisk. Run once at array start via # array_started.sh (System_Essentials/). Idempotent — already-mounted ramdisk # reports status and exits cleanly. Always resets the symlink to the ramdisk # on boot, ensuring a clean state regardless of what state it was in before # shutdown. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # Creates four things in order: # 1. RAMDISK_PATH — tmpfs mount (size: HOST*_RAMDISK_SIZE ceiling, not a reservation) # 2. TRANSCODE_SSD — SSD fallback directory and transcoding-temp inside it # 3. TRANSCODE_LINK — symlink reset to RAMDISK_PATH (clean state at every boot) # 4. transcoding-temp/ inside RAMDISK_PATH — pre-created before Emby starts # # The transcoding-temp pre-creation is critical: if it doesn't exist on the ramdisk # when Emby starts, Emby searches all accessible paths for an existing one and finds # the SSD fallback version — routing all sessions there until Emby restarts. # # Initialises /tmp/transcode_state.db with current target and flip counters. # /tmp resets on reboot — correct, transcode state should not persist across boots. # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Size Is a Ceiling, Not a Reservation # tmpfs allocates on write. RAMDISK_SIZE caps how large the ramdisk may grow; it does # not take that RAM away from the system up front. Sizing it generously costs nothing # until transcodes actually fill it, which is why the ceiling can sit well above # normal usage without starving anything. # # Clean Symlink State Every Boot # TRANSCODE_LINK is reset to the ramdisk at every array start rather than left wherever # the last flip put it. transcode_manager.sh flips it to SSD under pressure, and that # flip is a runtime response to a full ramdisk — carrying it across a reboot would mean # starting on the fallback with an empty ramdisk sitting unused. # # Pre-Create Before Emby Starts # transcoding-temp/ is created on the ramdisk before any container launches. Emby # searches accessible paths for an existing transcoding-temp at startup and binds to # the first it finds — if only the SSD copy exists, every session lands there until # Emby is restarted. Ordering here is not cosmetic; it decides where transcodes go. # # Idempotent Re-Runs # An already-mounted ramdisk is left mounted and only the symlink and permissions are # verified. Re-running never tears down a mount that active sessions are writing into. # # State Belongs in /tmp # The transcode state DB lives in /tmp and resets on reboot. Flip counters and the # current target describe a running system; carrying them across a boot would make the # manager act on pressure that no longer exists. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Required # mount and symlink creation require root. # # Single Instance Lock # acquire_lock prevents duplicate runs at array start. # # Idempotent Mount Check # If RAMDISK_PATH is already a mountpoint, reports status and exits cleanly # without attempting to remount or changing anything. # # Silent on Success # Startup script runs on every boot — no output when healthy. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # host*.conf # # HOST*_RAMDISK_SIZE # tmpfs ceiling (e.g. 10G). Must change together with WARN_GB and LOW_GB. # Aliased by detect_hosts() → RAMDISK_SIZE. # # HOST*_RAMDISK_WARN_GB # Usage level at which transcode_manager.sh flips symlink to SSD. # # HOST*_RAMDISK_LOW_GB # Usage level at which transcode_manager.sh flips back to ramdisk. # # HOST*_TRANSCODE_SSD # SSD fallback directory path. # Aliased by detect_hosts() → TRANSCODE_SSD. # # master.conf # # TRANSCODE_LINK # Symlink path Emby uses as its transcode directory. Must match the path # configured in Emby's transcoding settings. # # TRANSCODE_CHMOD / TRANSCODE_OWNER # Permissions applied to both ramdisk and SSD directories. (default: 755 / nobody:users) # # TRANSCODE_STATE_FILE # Override state file path. (default: ${STATE_DIR}/transcode_state.db) # # ============================================================================================== # STATE FILES # ============================================================================================== # # TRANSCODE_STATE_FILE (default: ${STATE_DIR}/transcode_state.db) # Current symlink target + flip count tracking. Lives in STATE_DIR # (ephemeral on Unraid — resets on reboot correctly). # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # ramdisk_setup.sh # Normal setup run. Called by array_started.sh at boot. # # ramdisk_setup.sh --dry-run # Show what would be created without creating anything. # # ramdisk_setup.sh --status # Show current ramdisk mount state, symlink target, and SSD directory state. # # ramdisk_setup.sh --log # Verbose output showing each creation step. # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" parse_args "$@" # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== if [[ "$EUID" -ne 0 ]]; then error "Must be run as root — mount and symlink require root" exit 1 fi acquire_lock # detect_hosts() sets MY_ID and aliases RAMDISK_SIZE, TRANSCODE_SSD etc. detect_hosts # This script mounts a tmpfs over RAMDISK_PATH and, when TRANSCODE_LINK exists but is not a # symlink, rm -rf's it before replacing it. Neither of those checks catches a collapsed path: # / and /mnt both satisfy -e, and mounting a tmpfs over a system directory hides its contents # for the life of the mount. Require at least two path components before either is touched. for _tc_pair in "RAMDISK_PATH:$RAMDISK_PATH" "TRANSCODE_LINK:$TRANSCODE_LINK"; do _tc_name="${_tc_pair%%:*}" _tc_path="${_tc_pair#*:}" _tc_slashes="${_tc_path//[^\/]/}" if [[ -z "$_tc_path" || "$_tc_path" != /* || "${#_tc_slashes}" -lt 2 ]]; then error "$_tc_name is unset or unsafe ('${_tc_path:-unset}') — refusing to mount or relink" notify "Ramdisk setup aborted on $(hostname) ($MY_ID) — $_tc_name is '${_tc_path:-unset}'" \ "Ramdisk Setup" "warning" exit 1 fi done unset _tc_pair _tc_name _tc_path _tc_slashes log "Identity: $MY_ID ($LOCAL_SERVER_NAME)" log "Ramdisk: $RAMDISK_PATH ($RAMDISK_SIZE)" log "Fallback: $TRANSCODE_SSD" log "Symlink: $TRANSCODE_LINK" [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made" # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_RAM Ramdisk path: $RAMDISK_PATH" echo "$ICON_RAM Ramdisk size: $RAMDISK_SIZE" echo "$ICON_RAM Warn at: ${RAMDISK_WARN_GB}GB" echo "$ICON_RAM Flip at: ${RAMDISK_LOW_GB}GB" echo "$ICON_DISK SSD fallback: $TRANSCODE_SSD" echo "$ICON_LINK Symlink: $TRANSCODE_LINK" echo "$ICON_GEAR Owner: $TRANSCODE_OWNER" echo "$ICON_GEAR Mode: $TRANSCODE_CHMOD" echo "" if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then # One df, not two — used and available come from the same read, so they cannot # describe two different moments of a ramdisk that is actively being written to. if read -r _ USED_MB AVAIL_MB <<< "$(disk_df "$RAMDISK_PATH")" && [[ -n "$AVAIL_MB" ]]; then echo " $ICON_RAM Ramdisk: mounted — $(format_mb "$USED_MB") used / $(format_mb "$AVAIL_MB") available ✅" else echo " $ICON_RAM Ramdisk: mounted — size unreadable" fi else echo " $ICON_RAM Ramdisk: NOT mounted" fi if [[ -L "$TRANSCODE_LINK" ]]; then TARGET=$(readlink "$TRANSCODE_LINK") echo " $ICON_LINK Symlink: $TRANSCODE_LINK → $TARGET" else echo " $ICON_LINK Symlink: not set" fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi # ============================================================================================== # ━━━ Ramdisk ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_RAM Ramdisk — $MY_ID ━━━" log "$ICON_RAM Path: $RAMDISK_PATH" log "$ICON_RAM Size: $RAMDISK_SIZE" echo "" START=$(date +%s) SETUP_SUCCESS=true if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then if read -r _ USED_MB AVAIL_MB <<< "$(disk_df "$RAMDISK_PATH")" && [[ -n "$AVAIL_MB" ]]; then log "Ramdisk already mounted — $(format_mb "$USED_MB") used / $(format_mb "$AVAIL_MB") available" else log "Ramdisk already mounted — size unreadable" fi log "Skipping mount — verifying symlink and permissions" else if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would create $RAMDISK_PATH" warn "DRY RUN — would mount tmpfs ${RAMDISK_SIZE} at $RAMDISK_PATH" else log "Creating ramdisk mount point: $RAMDISK_PATH" mkdir -p "$RAMDISK_PATH" || { error "Failed to create $RAMDISK_PATH" notify "Ramdisk setup failed on $(hostname) ($MY_ID) — could not create mount point" \ "Ramdisk Setup" "warning" exit 1 } log "Mounting tmpfs ${RAMDISK_SIZE} at $RAMDISK_PATH..." if mount -t tmpfs -o size="$RAMDISK_SIZE" tmpfs "$RAMDISK_PATH"; then warn "Ramdisk mounted — ${RAMDISK_SIZE} at $RAMDISK_PATH ✅" else error "Failed to mount ramdisk at $RAMDISK_PATH" notify "Ramdisk setup failed on $(hostname) ($MY_ID) — mount failed" \ "Ramdisk Setup" "warning" exit 1 fi fi fi # ============================================================================================== # ━━━ SSD Fallback ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_DISK SSD Fallback ━━━" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would create SSD fallback: $TRANSCODE_SSD" else if [[ -d "$TRANSCODE_SSD" ]]; then log "SSD fallback already exists: $TRANSCODE_SSD" else log "Creating SSD fallback directory: $TRANSCODE_SSD" if mkdir -p "$TRANSCODE_SSD"; then echo "SSD fallback created: $TRANSCODE_SSD ✅" else error "Failed to create SSD fallback: $TRANSCODE_SSD" SETUP_SUCCESS=false fi fi fi # ============================================================================================== # ━━━ Symlink ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_LINK Symlink ━━━" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would set $TRANSCODE_LINK → $RAMDISK_PATH" else if [[ -L "$TRANSCODE_LINK" ]]; then CURRENT_TARGET=$(readlink "$TRANSCODE_LINK") if [[ "$CURRENT_TARGET" == "$RAMDISK_PATH" ]]; then log "Symlink already points to ramdisk — no change needed ✅" else log "Updating symlink: $CURRENT_TARGET → $RAMDISK_PATH" ln -sfn "$RAMDISK_PATH" "$TRANSCODE_LINK" || { error "Failed to update symlink" SETUP_SUCCESS=false } fi elif [[ -e "$TRANSCODE_LINK" ]]; then warn "$TRANSCODE_LINK exists but is not a symlink — removing and replacing" rm -rf "$TRANSCODE_LINK" ln -sfn "$RAMDISK_PATH" "$TRANSCODE_LINK" || { error "Failed to create symlink" SETUP_SUCCESS=false } else log "Creating symlink: $TRANSCODE_LINK → $RAMDISK_PATH" mkdir -p "$(dirname "$TRANSCODE_LINK")" ln -sfn "$RAMDISK_PATH" "$TRANSCODE_LINK" || { error "Failed to create symlink" SETUP_SUCCESS=false } fi [[ "$SETUP_SUCCESS" == true ]] && echo "Symlink: $TRANSCODE_LINK → $RAMDISK_PATH ✅" fi # ============================================================================================== # ━━━ Transcoding-temp Directory ━━━ # ============================================================================================== # Pre-created inside ramdisk so Emby always finds it there at session start. # Without this Emby creates it at its own first-writable path — which may be # SSD even when the symlink points at the ramdisk — locking all sessions onto SSD. echo "" echo "━━━ $ICON_GEAR Transcoding Temp Directory ━━━" TRANSCODE_TEMP_DIR="${RAMDISK_PATH}/transcoding-temp" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would create $TRANSCODE_TEMP_DIR" else if [[ -d "$TRANSCODE_TEMP_DIR" ]]; then log "transcoding-temp already exists on ramdisk" else if mkdir -p "$TRANSCODE_TEMP_DIR"; then echo "Created transcoding-temp on ramdisk ✅" else error "Failed to create transcoding-temp on ramdisk" SETUP_SUCCESS=false fi fi if [[ -d "$TRANSCODE_TEMP_DIR" ]]; then chmod "$TRANSCODE_CHMOD" "$TRANSCODE_TEMP_DIR" chown "$TRANSCODE_OWNER" "$TRANSCODE_TEMP_DIR" log "Permissions set on transcoding-temp ($TRANSCODE_CHMOD $TRANSCODE_OWNER)" fi fi # ============================================================================================== # ━━━ Permissions ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_GEAR Permissions ━━━" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would apply $TRANSCODE_CHMOD $TRANSCODE_OWNER to $RAMDISK_PATH and $TRANSCODE_SSD" else for path in "$RAMDISK_PATH" "$TRANSCODE_SSD"; do if [[ -d "$path" ]]; then chmod "$TRANSCODE_CHMOD" "$path" chown "$TRANSCODE_OWNER" "$path" log "Permissions set: $path ($TRANSCODE_CHMOD $TRANSCODE_OWNER)" fi done fi # ============================================================================================== # ━━━ Initialise State File ━━━ # ============================================================================================== if [[ "$DRY_RUN" == false ]]; then STATE_FILE="${TRANSCODE_STATE_FILE:-${STATE_DIR:-/tmp}/transcode_state.db}" NOW=$(date +%s) cat > "$STATE_FILE" <