#!/bin/bash # Choosing which drive an app is installed onto. # # The resolver already supported this — CFG__STORAGE names a location and # appDir sends data, compose and config there — and every app template ships the # field with a generated dropdown of the registered locations. What was missing # was making the choice effective AT INSTALL TIME. Until now the only routes # were editing the config by hand before installing, or installing onto the # default disk and then `app move`ing it, which copies the data twice. # # The choice arrives two ways, and they are the same choice: # # libreportal app install --storage= -> LP_INSTALL_STORAGE # the storage dropdown in the App Center install form -> config_variables # # Both are resolved to one answer here so there is a single code path. # # ORDERING is the whole difficulty. installApp does: # # 1. dockerConfigSetupToContainer silent creates the app dir at # appDir() and copies the # template config into it # 2. initializeAppVariables sources that config # ... # 3. dockerConfigSetupToContainer loud install # applies the form overrides # # The choice has to be live before (1) or the directory is created on the wrong # disk; written into the config between (1) and (2) or sourcing the fresh # template resets it to "default" and every later appDir in the same process # returns the primary root; and present in config_variables for (3) or that # pass writes "default" back over it. Miss any one and you get an app whose # directory and config disagree — which resolves correctly only for as long as # nothing sources the config. # The requested location, or empty for "no opinion" (the normal path). # The CLI flag wins over the form field when both are present. storageChoiceFor() { local app="$1" config_vars="${2:-}" [[ -n "$app" ]] || return 0 if [[ -n "${LP_INSTALL_STORAGE:-}" ]]; then printf '%s' "$LP_INSTALL_STORAGE" return 0 fi local key="CFG_${app^^}_STORAGE" pair local -a pairs=() IFS='|' read -ra pairs <<< "$config_vars" for pair in "${pairs[@]}"; do if [[ "$pair" == "$key="* ]]; then printf '%s' "${pair#$key=}" return 0 fi done return 0 } # Make appDir resolve to the chosen location, before anything is created. storagePlaceAppPre() { local app="$1" want="$2" [[ -n "$app" && -n "$want" ]] || return 0 # "default" is the templates' own value and means "follow CFG_STORAGE_DEFAULT", # so it is not a placement and must not be treated as one. [[ "$want" == "default" ]] && return 0 # Some apps are deliberately fixed to the primary root because other apps # reach them by literal path; their template marks the field **READONLY**. # storageMoveApp already refuses to move those, and installing one somewhere # else is the same violation arrived at from the other end — it just breaks # the referring app immediately instead of later. local tmpl="${install_containers_dir%/}/$app/$app.config" if [[ -f "$tmpl" ]] && grep -qE "^CFG_${app^^}_STORAGE=.*\*\*READONLY\*\*" "$tmpl" 2>/dev/null; then isError "$app is fixed to the default location — other apps reference it by path." isNotice "Install it without a location, or move the apps that depend on it instead." return 1 fi local root if [[ "$want" == "primary" ]]; then root=$(primaryRoot) elif ! root=$(storageLocationPath "$want"); then isError "No such storage location: $want" isNotice "Known locations: $(storageLocationNames 2>/dev/null | paste -sd, -)" return 1 fi if ! storageRootAvailable "$root"; then isError "Storage location '$want' ($root) is not mounted — refusing to install onto it." isNotice "Mount it first, or leave the location unset to use the default." return 1 fi if [[ -e "${root%/}/$app" ]]; then isError "'${root%/}/$app' already exists — refusing to install over it." return 1 fi # Two records answering at different times: the exported variable is what # _appDirIntended reads in THIS process before any config exists for the # app; the index is what answers for a later process whose config lives on # a drive that is not mounted. export "CFG_${app^^}_STORAGE=$want" storageIndexSet "$app" "$root" storageCacheReset isNotice "Installing $app onto '$want' ($root)." return 0 } # Keep the choice, now that there is a deployed config to keep it in. storagePlaceAppPost() { local app="$1" want="$2" [[ -n "$app" && -n "$want" && "$want" != "default" ]] || return 0 local dir cfg dir=$(appDir "$app") || return 0 cfg="$dir/$app.config" [[ -f "$cfg" ]] || return 0 updateConfigOption "CFG_${app^^}_STORAGE" "$want" "$cfg" >/dev/null 2>&1 || true export "CFG_${app^^}_STORAGE=$want" declare -f storageSyncAppComment >/dev/null 2>&1 && storageSyncAppComment "$app" return 0 } # config_variables with the choice set, so the later override pass writes it # rather than resetting it to the template's "default". storageChoiceMerge() { local app="$1" want="$2" config_vars="${3:-}" local key="CFG_${app^^}_STORAGE" [[ -n "$want" ]] || { printf '%s' "$config_vars"; return 0; } local -a keep=() pairs=() local pair IFS='|' read -ra pairs <<< "$config_vars" for pair in "${pairs[@]}"; do [[ -z "$pair" || "$pair" == "$key="* ]] && continue keep+=("$pair") done keep+=("$key=$want") local out="" p for p in "${keep[@]}"; do [[ -n "$out" ]] && out+="|" out+="$p" done printf '%s' "$out" } # Every location an app can be installed onto, one per line. storageLocationNames() { printf '%s\n' primary local root while IFS= read -r root; do [[ -z "$root" ]] && continue [[ "${root%/}" == "$(primaryRoot)" ]] && continue storageLocationName "$root" done < <(storageRoots 2>/dev/null) }