|
#!/usr/bin/env bash |
|
# |
|
# sync-branding-from-live.sh |
|
# |
|
# Fetch installation branding from a live Dataverse site (read-only) and install |
|
# it on local docker-compose-dev. The live server is never modified. Local install |
|
# uses LOCAL_URL only (default http://localhost:8080). |
|
# |
|
# See scripts/dev/sync-branding-from-live.md for usage. |
|
# |
|
set -euo pipefail |
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" |
|
|
|
# Keys loaded from ${REPO_ROOT}/.env when not already set in the shell |
|
BRANDING_ENV_KEYS=( |
|
LIVE_URL |
|
SSH_HOST |
|
LOCAL_URL |
|
DV_DATA_DIR |
|
SYNC_DIR |
|
ROOT_ALIAS |
|
REMOTE_DATAVERSE_URL |
|
REMOTE_BRANDING_DIR |
|
REMOTE_DOCROOT |
|
LIVE_API_TOKEN |
|
LOCAL_API_TOKEN |
|
DOCKER_CONTAINER |
|
REMOTE_DOCROOT_PATHS |
|
REMOTE_EXTRA_FILES |
|
) |
|
|
|
is_branding_env_key() { |
|
local want="$1" k |
|
for k in "${BRANDING_ENV_KEYS[@]}"; do |
|
[[ "$k" == "$want" ]] && return 0 |
|
done |
|
return 1 |
|
} |
|
|
|
# Load branding-related variables from repo .env (does not override non-empty env) |
|
load_repo_env() { |
|
local env_file="${REPO_ROOT}/.env" |
|
[[ -f "$env_file" ]] || return 0 |
|
|
|
local line key val existing |
|
while IFS= read -r line || [[ -n "$line" ]]; do |
|
line="$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" |
|
[[ -z "$line" ]] && continue |
|
[[ "$line" == \#* ]] && continue |
|
line="${line#export }" |
|
[[ "$line" == *"="* ]] || continue |
|
key="${line%%=*}" |
|
val="${line#*=}" |
|
is_branding_env_key "$key" || continue |
|
existing="$(printenv "$key" 2>/dev/null || true)" |
|
[[ -n "$existing" ]] && continue |
|
case "$val" in |
|
\"*\") val="${val#\"}"; val="${val%\"}" ;; |
|
\'*\') val="${val#\'}"; val="${val%\'}" ;; |
|
esac |
|
export "${key}=${val}" |
|
done < "$env_file" |
|
} |
|
|
|
load_repo_env |
|
|
|
# --- defaults (after .env; empty values still get defaults) --- |
|
LIVE_URL="${LIVE_URL:-}" |
|
SSH_HOST="${SSH_HOST:-}" |
|
LOCAL_URL="${LOCAL_URL:-http://localhost:8080}" |
|
DV_DATA_DIR="${DV_DATA_DIR:-${REPO_ROOT}/docker-dev-volumes/app/data}" |
|
SYNC_DIR="${SYNC_DIR:-${DV_DATA_DIR}/branding-sync}" |
|
ROOT_ALIAS="${ROOT_ALIAS:-root}" |
|
REMOTE_DATAVERSE_URL="${REMOTE_DATAVERSE_URL:-http://127.0.0.1:8080}" |
|
REMOTE_BRANDING_DIR="${REMOTE_BRANDING_DIR:-}" |
|
REMOTE_DOCROOT="${REMOTE_DOCROOT:-}" |
|
REMOTE_DOCROOT_PATHS="${REMOTE_DOCROOT_PATHS:-css/main.css}" |
|
# remote_path:host_path_under_DV_DATA_DIR (JHU navbar logo is often under Apache, not Payara docroot) |
|
REMOTE_EXTRA_FILES="${REMOTE_EXTRA_FILES:-/var/www/html/images/libraries.logo.small.horizontal.white.cropped.png:docroot/images/libraries.logo.small.horizontal.white.cropped.png}" |
|
DOCKER_CONTAINER="${DOCKER_CONTAINER:-dev_dataverse}" |
|
LIVE_API_TOKEN="${LIVE_API_TOKEN:-}" |
|
LOCAL_API_TOKEN="${LOCAL_API_TOKEN:-}" |
|
|
|
SKIP_LOCAL_APPLY=false |
|
APPLY_ONLY=false |
|
WITH_THEME=false |
|
WITH_DOCROOT=false |
|
CLOSER_MODE=false |
|
SKIP_ANALYTICS=false |
|
DRY_RUN=false |
|
ASSUME_YES=false |
|
|
|
# Whitelisted branding settings (never bulk-import all admin settings) |
|
BRANDING_KEYS=( |
|
':HomePageCustomizationFile' |
|
':HeaderCustomizationFile' |
|
':FooterCustomizationFile' |
|
':StyleCustomizationFile' |
|
':LogoCustomizationFile' |
|
':WebAnalyticsCode' |
|
':NavbarAboutUrl' |
|
':NavbarGuidesUrl' |
|
':NavbarSupportUrl' |
|
':FooterCopyright' |
|
':InstallationName' |
|
':DisableRootDataverseTheme' |
|
':Languages' |
|
) |
|
|
|
FILE_SETTING_KEYS=( |
|
':HomePageCustomizationFile' |
|
':HeaderCustomizationFile' |
|
':FooterCustomizationFile' |
|
':StyleCustomizationFile' |
|
':LogoCustomizationFile' |
|
':WebAnalyticsCode' |
|
) |
|
|
|
# customization API type for a setting key |
|
custom_type_for_key() { |
|
case "$1" in |
|
':HomePageCustomizationFile') echo homePage ;; |
|
':HeaderCustomizationFile') echo header ;; |
|
':FooterCustomizationFile') echo footer ;; |
|
':StyleCustomizationFile') echo style ;; |
|
':LogoCustomizationFile') echo logo ;; |
|
':WebAnalyticsCode') echo analytics ;; |
|
*) echo "" ;; |
|
esac |
|
} |
|
|
|
MANIFEST_JSON="${SYNC_DIR}/manifest.json" |
|
SETTINGS_JSON="${SYNC_DIR}/live-branding-settings.json" |
|
FILES_DIR="${SYNC_DIR}/files" |
|
|
|
usage() { |
|
cat <<'EOF' |
|
Usage: sync-branding-from-live.sh [OPTIONS] |
|
|
|
Fetch branding from a live Dataverse site (HTTPS + SSH, read-only) and |
|
optionally install it on local docker-compose-dev. The live server is never |
|
modified. |
|
|
|
Required (unless --apply-only): |
|
LIVE_URL Public live site URL (e.g. https://dataverse.example.edu) |
|
SSH_HOST SSH target for admin settings (e.g. user@prod-server) |
|
|
|
Options: |
|
--no-apply Fetch from live and stage only; do not update local Docker |
|
--apply-only Install from existing branding-sync/; do not fetch from live |
|
--with-theme Also pull root collection theme images and metadata |
|
--with-docroot Pull Payara docroot + Apache static files (css, fonts, navbar logo) |
|
--closer Max fidelity for JHU-like sites (implies --with-docroot): extra |
|
settings, URL rewrites, main.css merge, root name, logo path |
|
--skip-analytics Skip :WebAnalyticsCode pull/apply |
|
--dry-run Print actions without writing or applying |
|
--yes Skip confirmation before apply |
|
-h, --help Show this help |
|
|
|
Environment (also read from repo .env if unset in shell): |
|
LIVE_URL, SSH_HOST, LOCAL_URL, DV_DATA_DIR, SYNC_DIR, ROOT_ALIAS, |
|
REMOTE_DATAVERSE_URL, REMOTE_BRANDING_DIR, REMOTE_DOCROOT, REMOTE_DOCROOT_PATHS, |
|
REMOTE_EXTRA_FILES, LIVE_API_TOKEN, LOCAL_API_TOKEN, DOCKER_CONTAINER |
|
|
|
Shell exports override .env. See scripts/dev/sync-branding-from-live.md |
|
|
|
EOF |
|
} |
|
|
|
log() { echo "==> $*"; } |
|
warn() { echo "WARNING: $*" >&2; } |
|
die() { echo "ERROR: $*" >&2; exit 1; } |
|
|
|
require_deps() { |
|
local dep |
|
for dep in curl jq ssh scp; do |
|
command -v "$dep" >/dev/null 2>&1 || die "Missing required command: $dep" |
|
done |
|
} |
|
|
|
is_file_setting() { |
|
local key="$1" |
|
local k |
|
for k in "${FILE_SETTING_KEYS[@]}"; do |
|
[[ "$k" == "$key" ]] && return 0 |
|
done |
|
return 1 |
|
} |
|
|
|
# Map production path to container path used in local settings |
|
map_to_local_container_path() { |
|
local key="$1" value="$2" |
|
local basename |
|
basename="$(basename "$value")" |
|
|
|
case "$key" in |
|
':LogoCustomizationFile') |
|
if [[ "$value" == /logos/* ]]; then |
|
echo "$value" |
|
else |
|
echo "/logos/navbar/${basename}" |
|
fi |
|
;; |
|
':HomePageCustomizationFile'|':HeaderCustomizationFile'|':FooterCustomizationFile'|':StyleCustomizationFile'|':WebAnalyticsCode') |
|
echo "/dv/branding/${basename}" |
|
;; |
|
*) |
|
echo "$value" |
|
;; |
|
esac |
|
} |
|
|
|
# Host filesystem path under DV_DATA_DIR |
|
host_path_for_container_path() { |
|
local container_path="$1" |
|
if [[ "$container_path" == /dv/* ]]; then |
|
echo "${DV_DATA_DIR}${container_path#/dv}" |
|
elif [[ "$container_path" == /logos/* ]]; then |
|
echo "${DV_DATA_DIR}/docroot${container_path}" |
|
else |
|
echo "${DV_DATA_DIR}/${container_path#/}" |
|
fi |
|
} |
|
|
|
run() { |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] $*" |
|
else |
|
"$@" |
|
fi |
|
} |
|
|
|
curl_live_get() { |
|
local url="$1" |
|
local extra=() |
|
[[ -n "$LIVE_API_TOKEN" ]] && extra+=(-H "X-Dataverse-key: ${LIVE_API_TOKEN}") |
|
curl -fsS "${extra[@]}" "$url" |
|
} |
|
|
|
curl_local_put() { |
|
local url="$1" |
|
local data="$2" |
|
local extra=() |
|
[[ -n "$LOCAL_API_TOKEN" ]] && extra+=(-H "X-Dataverse-key: ${LOCAL_API_TOKEN}") |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] curl -X PUT ${url}" |
|
echo "[dry-run] body: ${data:0:120}$([[ ${#data} -gt 120 ]] && echo ...)" |
|
return 0 |
|
fi |
|
curl -fsS "${extra[@]}" -X PUT "$url" -d "$data" >/dev/null |
|
} |
|
|
|
# --- pull: SSH admin settings (GET only on server) --- |
|
pull_remote_settings() { |
|
log "Fetching branding settings via SSH (${SSH_HOST}) -> ${REMOTE_DATAVERSE_URL}/api/admin/settings" |
|
|
|
local remote_filter |
|
remote_filter="$(printf '%s\n' "${BRANDING_KEYS[@]}" | jq -R . | jq -s .)" |
|
|
|
local tmp |
|
tmp="$(mktemp)" |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] ssh ${SSH_HOST} curl -fsS ${REMOTE_DATAVERSE_URL}/api/admin/settings" |
|
echo '{}' > "$tmp" |
|
else |
|
if ! ssh -o BatchMode=yes "${SSH_HOST}" bash -s -- "${REMOTE_DATAVERSE_URL}" <<'REMOTE' >"$tmp" |
|
set -euo pipefail |
|
REMOTE_URL="$1" |
|
# Safety: remote helper only performs GET |
|
curl -fsS "${REMOTE_URL}/api/admin/settings" |
|
REMOTE |
|
then |
|
die "SSH admin settings pull failed. Check SSH_HOST and that admin API allows localhost on the server." |
|
fi |
|
fi |
|
|
|
if [[ "$DRY_RUN" != true ]]; then |
|
jq --argjson keys "$remote_filter" ' |
|
reduce $keys[] as $k ({}; if .data[$k] != null then . + { ($k): .data[$k] } else . end) |
|
' "$tmp" > "${SETTINGS_JSON}.raw" 2>/dev/null || true |
|
|
|
if jq -e '.data' "$tmp" >/dev/null 2>&1; then |
|
jq --argjson keys "$remote_filter" ' |
|
.data as $d | |
|
reduce $keys[] as $k ({}; if $d[$k] != null then . + { ($k): $d[$k] } else . end) |
|
' "$tmp" > "$SETTINGS_JSON" |
|
elif jq -e 'type == "object"' "$tmp" >/dev/null 2>&1; then |
|
jq --argjson keys "$remote_filter" ' |
|
. as $d | |
|
reduce $keys[] as $k ({}; if $d[$k] != null then . + { ($k): $d[$k] } else . end) |
|
' "$tmp" > "$SETTINGS_JSON" |
|
else |
|
die "Unexpected response from remote admin settings API" |
|
fi |
|
local n |
|
n="$(jq 'length' "$SETTINGS_JSON")" |
|
run cp "$tmp" "${SYNC_DIR}/live-admin-settings-raw.json" |
|
log "Wrote ${SETTINGS_JSON} (${n} branding setting(s))" |
|
if [[ "$n" -eq 0 ]]; then |
|
warn "Admin API returned no whitelisted branding keys (see live-admin-settings-raw.json)." |
|
warn "Try --with-theme if look-and-feel comes from the root collection theme." |
|
fi |
|
rm -f "$tmp" "${SETTINGS_JSON}.raw" |
|
fi |
|
} |
|
|
|
# Add stock settings derived from JHU-only keys in live-admin-settings-raw.json |
|
merge_closer_settings_from_raw() { |
|
local raw="${SYNC_DIR}/live-admin-settings-raw.json" |
|
[[ -f "$raw" ]] || return 0 |
|
|
|
log "Merging closer settings from raw admin API (stock keys only)" |
|
local inst_name disable_root logo_path |
|
inst_name="$(jq -r '.data[":instanceNameFull"] // empty' "$raw")" |
|
disable_root="$(jq -r '.data[":instanceBrandingHeader"] // empty' "$raw")" |
|
logo_path="/logos/navbar/libraries.logo.small.horizontal.white.cropped.png" |
|
|
|
local merged |
|
merged="$(jq '.' "$SETTINGS_JSON")" |
|
if [[ -n "$inst_name" && "$inst_name" != "null" ]]; then |
|
merged="$(echo "$merged" | jq --arg v "$inst_name" '. + {":InstallationName": $v}')" |
|
fi |
|
if [[ "$disable_root" == "true" ]]; then |
|
merged="$(echo "$merged" | jq '. + {":DisableRootDataverseTheme": "true"}')" |
|
fi |
|
if [[ -f "${DV_DATA_DIR}/docroot/logos/navbar/libraries.logo.small.horizontal.white.cropped.png" ]] \ |
|
|| [[ -f "${DV_DATA_DIR}/docroot/images/libraries.logo.small.horizontal.white.cropped.png" ]]; then |
|
merged="$(echo "$merged" | jq --arg v "$logo_path" '. + {":LogoCustomizationFile": $v}')" |
|
fi |
|
echo "$merged" > "$SETTINGS_JSON" |
|
} |
|
|
|
extension_for_content_type() { |
|
local ct="$1" |
|
case "$ct" in |
|
*html*) echo ".html" ;; |
|
*css*) echo ".css" ;; |
|
*png*) echo ".png" ;; |
|
*jpeg*|*jpg*) echo ".jpg" ;; |
|
*svg*) echo ".svg" ;; |
|
*javascript*) echo ".js" ;; |
|
*) echo "" ;; |
|
esac |
|
} |
|
|
|
# --- pull: HTTPS customization API from laptop --- |
|
pull_customization_https() { |
|
local ctype="$1" |
|
local out_base="$2" |
|
|
|
local tmp_headers tmp_body http_code content_type ext |
|
tmp_headers="$(mktemp)" |
|
tmp_body="$(mktemp)" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] curl ${LIVE_URL}/api/info/settings/customization/${ctype}" |
|
rm -f "$tmp_headers" "$tmp_body" |
|
return 1 |
|
fi |
|
|
|
http_code="$(curl -sS -D "$tmp_headers" -o "$tmp_body" -w '%{http_code}' \ |
|
"${LIVE_URL}/api/info/settings/customization/${ctype}" || echo "000")" |
|
|
|
if [[ "$http_code" != "200" ]]; then |
|
rm -f "$tmp_headers" "$tmp_body" |
|
log " (skip) customization API '${ctype}' HTTP ${http_code} (will use SSH file copy if configured)" |
|
return 1 |
|
fi |
|
|
|
content_type="$(grep -i '^content-type:' "$tmp_headers" | head -1 | cut -d: -f2- | tr -d '[:space:]' | cut -d';' -f1)" |
|
ext="$(extension_for_content_type "$content_type")" |
|
[[ -z "$ext" ]] && ext=".bin" |
|
|
|
local out_file="${out_base}${ext}" |
|
run mkdir -p "$(dirname "$out_file")" |
|
run cp "$tmp_body" "$out_file" |
|
rm -f "$tmp_headers" "$tmp_body" |
|
log " saved customization ${ctype} -> ${out_file}" |
|
echo "$out_file" |
|
} |
|
|
|
# Resolve Payara docroot on the server (JHU may use payara6, payara7, or payara) |
|
resolve_remote_docroot() { |
|
if [[ -n "$REMOTE_DOCROOT" ]]; then |
|
echo "$REMOTE_DOCROOT" |
|
return 0 |
|
fi |
|
local candidate |
|
for candidate in \ |
|
/usr/local/payara7/glassfish/domains/domain1/docroot \ |
|
/usr/local/payara6/glassfish/domains/domain1/docroot \ |
|
/usr/local/payara/glassfish/domains/domain1/docroot; do |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "$candidate" |
|
return 0 |
|
fi |
|
if ssh -o BatchMode=yes "${SSH_HOST}" "test -d $(printf '%q' "$candidate")" 2>/dev/null; then |
|
echo "$candidate" |
|
return 0 |
|
fi |
|
done |
|
return 1 |
|
} |
|
|
|
# Extra static files outside Payara docroot (e.g. Apache /var/www/html/images/...) |
|
pull_extra_static_files() { |
|
local pair remote local_path |
|
[[ -z "$REMOTE_EXTRA_FILES" ]] && return 0 |
|
log "Fetching extra static files (SSH)" |
|
|
|
IFS=',' read -ra pairs <<< "$REMOTE_EXTRA_FILES" |
|
for pair in "${pairs[@]}"; do |
|
pair="$(printf '%s' "$pair" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" |
|
[[ -z "$pair" || "$pair" != *:* ]] && continue |
|
remote="${pair%%:*}" |
|
local_path="${DV_DATA_DIR}/${pair#*:}" |
|
pull_ssh_file "$remote" "$local_path" || true |
|
done |
|
} |
|
|
|
# Payara docroot static files (e.g. /css/main.css) — not under /var/www/dataverse/branding |
|
pull_docroot_assets() { |
|
local docroot rel local_path |
|
docroot="$(resolve_remote_docroot)" || { |
|
warn "Could not find Payara docroot on server; set REMOTE_DOCROOT in .env" |
|
return 1 |
|
} |
|
log "Fetching docroot assets from ${docroot} (SSH)" |
|
|
|
IFS=',' read -ra rel_paths <<< "$REMOTE_DOCROOT_PATHS" |
|
for rel in "${rel_paths[@]}"; do |
|
rel="$(printf '%s' "$rel" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" |
|
[[ -z "$rel" ]] && continue |
|
local_path="${DV_DATA_DIR}/docroot/${rel}" |
|
pull_ssh_file "${docroot}/${rel}" "$local_path" || true |
|
done |
|
|
|
if [[ "$CLOSER_MODE" == true ]]; then |
|
pull_docroot_fonts "$docroot" |
|
fi |
|
} |
|
|
|
pull_docroot_fonts() { |
|
local docroot="$1" |
|
local remote_fonts="${docroot}/fonts" |
|
local local_fonts="${DV_DATA_DIR}/docroot/fonts" |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] scp -r ${SSH_HOST}:${remote_fonts}/ -> ${local_fonts}/" |
|
return 0 |
|
fi |
|
if ssh -o BatchMode=yes "${SSH_HOST}" "test -d $(printf '%q' "$remote_fonts")" 2>/dev/null; then |
|
run mkdir -p "$local_fonts" |
|
scp -qr "${SSH_HOST}:${remote_fonts}/." "$local_fonts/" |
|
log " ${remote_fonts}/ -> ${local_fonts}/" |
|
else |
|
warn " no fonts directory at ${remote_fonts}" |
|
fi |
|
} |
|
|
|
# Copy branding files from absolute paths in live settings (SSH read-only) |
|
pull_branding_files_via_ssh() { |
|
local key val basename dest |
|
for key in "${FILE_SETTING_KEYS[@]}"; do |
|
if [[ "$SKIP_ANALYTICS" == true && "$key" == ':WebAnalyticsCode' ]]; then |
|
continue |
|
fi |
|
val="$(jq -r --arg k "$key" '.[$k] // empty' "$SETTINGS_JSON")" |
|
[[ -z "$val" || "$val" == "null" || "$val" != /* ]] && continue |
|
basename="$(basename "$val")" |
|
dest="${FILES_DIR}/${basename}" |
|
pull_ssh_file "$val" "$dest" || true |
|
done |
|
} |
|
|
|
# --- pull: SSH read-only file fallback --- |
|
pull_ssh_file() { |
|
local remote_path="$1" |
|
local local_path="$2" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] scp ${SSH_HOST}:${remote_path} -> ${local_path}" |
|
return 0 |
|
fi |
|
|
|
if ssh -o BatchMode=yes "${SSH_HOST}" "test -r $(printf '%q' "$remote_path")"; then |
|
run mkdir -p "$(dirname "$local_path")" |
|
scp -q "${SSH_HOST}:${remote_path}" "$local_path" |
|
log " ${remote_path} -> ${local_path}" |
|
return 0 |
|
fi |
|
warn " ssh fallback: not readable on server: ${remote_path}" |
|
return 1 |
|
} |
|
|
|
# --- pull: root theme (--with-theme) --- |
|
pull_root_theme() { |
|
log "Fetching root collection theme (${ROOT_ALIAS})" |
|
|
|
local theme_json="${SYNC_DIR}/root-theme.json" |
|
local live_dv_json="${SYNC_DIR}/live-root-dataverse.json" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] GET ${LIVE_URL}/api/dataverses/${ROOT_ALIAS}" |
|
return 0 |
|
fi |
|
|
|
curl_live_get "${LIVE_URL}/api/dataverses/${ROOT_ALIAS}" > "$live_dv_json" |
|
jq '.data.theme // empty' "$live_dv_json" > "$theme_json" |
|
|
|
if [[ ! -s "$theme_json" ]]; then |
|
log " no theme configured on live root" |
|
return 0 |
|
fi |
|
log " wrote ${theme_json}" |
|
|
|
local live_id logo logo_footer logo_thumb |
|
live_id="$(jq -r '.data.id' "$live_dv_json")" |
|
logo="$(jq -r '.logo // empty' "$theme_json")" |
|
logo_footer="$(jq -r '.logoFooter // empty' "$theme_json")" |
|
logo_thumb="$(jq -r '.logoThumbnail // empty' "$theme_json")" |
|
|
|
local local_id="1" |
|
if curl -fsS "${LOCAL_URL}/api/dataverses/${ROOT_ALIAS}" >/dev/null 2>&1; then |
|
local_id="$(curl -fsS "${LOCAL_URL}/api/dataverses/${ROOT_ALIAS}" | jq -r '.data.id // 1')" |
|
fi |
|
log " live root id=${live_id}, local root id=${local_id}" |
|
|
|
download_theme_logo() { |
|
local file="$1" |
|
[[ -z "$file" || "$file" == "null" ]] && return 0 |
|
local dest="${DV_DATA_DIR}/docroot/logos/${local_id}/${file}" |
|
run mkdir -p "$(dirname "$dest")" |
|
log " downloading ${LIVE_URL}/logos/${live_id}/${file}" |
|
run curl -fsS "${LIVE_URL}/logos/${live_id}/${file}" -o "$dest" |
|
echo "$dest" |
|
} |
|
|
|
download_theme_logo "$logo" >/dev/null || true |
|
download_theme_logo "$logo_footer" >/dev/null || true |
|
download_theme_logo "$logo_thumb" >/dev/null || true |
|
|
|
jq -n \ |
|
--argjson theme "$(cat "$theme_json")" \ |
|
--arg localId "$local_id" \ |
|
--arg liveId "$live_id" \ |
|
'{ theme: $theme, localRootId: $localId, liveRootId: $liveId }' \ |
|
> "${SYNC_DIR}/theme-meta.json" |
|
} |
|
|
|
# Build manifest from settings + pulled files |
|
build_manifest() { |
|
log "Building manifest" |
|
|
|
if [[ ! -f "$SETTINGS_JSON" ]]; then |
|
die "Missing ${SETTINGS_JSON}; run pull first" |
|
fi |
|
|
|
local settings_obj="{}" |
|
local key val local_path host_path staged rel_staged |
|
local -a entries=() |
|
|
|
for key in "${BRANDING_KEYS[@]}"; do |
|
if [[ "$SKIP_ANALYTICS" == true && "$key" == ':WebAnalyticsCode' ]]; then |
|
continue |
|
fi |
|
|
|
val="$(jq -r --arg k "$key" '.[$k] // empty' "$SETTINGS_JSON")" |
|
[[ -z "$val" || "$val" == "null" ]] && continue |
|
|
|
local_path="$(map_to_local_container_path "$key" "$val")" |
|
host_path="$(host_path_for_container_path "$local_path")" |
|
staged="" |
|
rel_staged="" |
|
|
|
if is_file_setting "$key"; then |
|
local ctype |
|
ctype="$(custom_type_for_key "$key")" |
|
if [[ -n "$ctype" ]]; then |
|
local staged_glob="${FILES_DIR}/${ctype}.*" |
|
# shellcheck disable=SC2086 |
|
for f in $staged_glob; do |
|
[[ -f "$f" ]] && staged="$f" && break |
|
done |
|
fi |
|
|
|
if [[ -z "$staged" && "$val" == /* ]]; then |
|
local fallback="${FILES_DIR}/$(basename "$val")" |
|
if [[ ! -f "$fallback" ]]; then |
|
pull_ssh_file "$val" "$fallback" || true |
|
fi |
|
[[ -f "$fallback" ]] && staged="$fallback" |
|
fi |
|
|
|
if [[ -n "$staged" ]]; then |
|
rel_staged="${staged#${SYNC_DIR}/}" |
|
fi |
|
fi |
|
|
|
entries+=("$(jq -n \ |
|
--arg key "$key" \ |
|
--arg liveValue "$val" \ |
|
--arg localPath "$local_path" \ |
|
--arg hostPath "$host_path" \ |
|
--arg staged "$rel_staged" \ |
|
--argjson isFile "$(is_file_setting "$key" && echo true || echo false)" \ |
|
'{ |
|
key: $key, |
|
liveValue: $liveValue, |
|
localContainerPath: $localPath, |
|
hostPath: $hostPath, |
|
stagedFile: (if $staged == "" then null else $staged end), |
|
isFile: $isFile |
|
}')") |
|
done |
|
|
|
local entries_json="[]" |
|
if [[ ${#entries[@]} -gt 0 ]]; then |
|
entries_json="$(printf '%s\n' "${entries[@]}" | jq -s '.')" |
|
else |
|
warn "No branding database settings found on live (empty ${SETTINGS_JSON})." |
|
if [[ "$WITH_THEME" != true ]]; then |
|
warn "If look-and-feel is from the root collection banner, re-run with --with-theme." |
|
fi |
|
fi |
|
|
|
jq -n \ |
|
--arg liveUrl "$LIVE_URL" \ |
|
--arg sshHost "$SSH_HOST" \ |
|
--arg syncDir "$SYNC_DIR" \ |
|
--arg dvDataDir "$DV_DATA_DIR" \ |
|
--argjson withTheme "$( [[ "$WITH_THEME" == true ]] && echo true || echo false )" \ |
|
--argjson withDocroot "$( [[ "$WITH_DOCROOT" == true || "$CLOSER_MODE" == true ]] && echo true || echo false )" \ |
|
--argjson closer "$( [[ "$CLOSER_MODE" == true ]] && echo true || echo false )" \ |
|
--argjson settings "$entries_json" \ |
|
'{ liveUrl: $liveUrl, sshHost: $sshHost, syncDir: $syncDir, dvDataDir: $dvDataDir, withTheme: $withTheme, withDocroot: $withDocroot, closer: $closer, entries: $settings }' \ |
|
> "$MANIFEST_JSON" |
|
|
|
log "Wrote ${MANIFEST_JSON}" |
|
} |
|
|
|
pull_phase() { |
|
[[ -n "$LIVE_URL" ]] || die "LIVE_URL is required for pull" |
|
[[ -n "$SSH_HOST" ]] || die "SSH_HOST is required for pull" |
|
|
|
LIVE_URL="${LIVE_URL%/}" |
|
LOCAL_URL="${LOCAL_URL%/}" |
|
|
|
log "Fetch from live (read-only): ${LIVE_URL} via ${SSH_HOST} -> ${SYNC_DIR}" |
|
run mkdir -p "$FILES_DIR" "${DV_DATA_DIR}/branding" "${DV_DATA_DIR}/docroot/logos/navbar" |
|
|
|
pull_remote_settings |
|
|
|
if [[ -f "$SETTINGS_JSON" ]] && [[ "$(jq 'length' "$SETTINGS_JSON")" -gt 0 ]]; then |
|
log "Fetching branding files from server paths (SSH)" |
|
pull_branding_files_via_ssh |
|
fi |
|
|
|
if [[ "$DRY_RUN" != true ]]; then |
|
log "Trying customization API over HTTPS (optional)" |
|
local key ctype |
|
for key in "${FILE_SETTING_KEYS[@]}"; do |
|
if [[ "$SKIP_ANALYTICS" == true && "$key" == ':WebAnalyticsCode' ]]; then |
|
continue |
|
fi |
|
ctype="$(custom_type_for_key "$key")" |
|
[[ -n "$ctype" ]] || continue |
|
pull_customization_https "$ctype" "${FILES_DIR}/${ctype}" || true |
|
done |
|
else |
|
for key in "${FILE_SETTING_KEYS[@]}"; do |
|
ctype="$(custom_type_for_key "$key")" |
|
[[ -n "$ctype" ]] || continue |
|
pull_customization_https "$ctype" "${FILES_DIR}/${ctype}" || true |
|
done |
|
fi |
|
|
|
if [[ "$WITH_THEME" == true ]]; then |
|
pull_root_theme |
|
fi |
|
|
|
if [[ "$WITH_DOCROOT" == true || "$CLOSER_MODE" == true ]]; then |
|
pull_docroot_assets |
|
pull_extra_static_files |
|
fi |
|
|
|
if [[ "$CLOSER_MODE" == true ]]; then |
|
merge_closer_settings_from_raw |
|
fi |
|
|
|
if [[ "$DRY_RUN" != true ]]; then |
|
build_manifest |
|
else |
|
log "[dry-run] would build ${MANIFEST_JSON}" |
|
fi |
|
} |
|
|
|
# Rewrite production hostnames in pulled HTML to LOCAL_URL |
|
adapt_production_urls_in_branding() { |
|
local live_host local_host f |
|
live_host="$(printf '%s' "$LIVE_URL" | sed -E 's#^https?://##;s#/.*##')" |
|
local_host="$(printf '%s' "$LOCAL_URL" | sed -E 's#^https?://##;s#/.*##')" |
|
[[ -z "$live_host" || "$live_host" == "$local_host" ]] && return 0 |
|
|
|
log "Rewriting ${live_host} -> ${local_host} in branding HTML" |
|
for f in "${DV_DATA_DIR}/branding/"*.html; do |
|
[[ -f "$f" ]] || continue |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] sed ${live_host} -> ${local_host} in ${f}" |
|
else |
|
sed "s|${live_host}|${local_host}|g" "$f" > "${f}.tmp" && mv "${f}.tmp" "$f" |
|
fi |
|
done |
|
} |
|
|
|
# main.css is not served at /css/* in stock Docker; merge into custom-stylesheet.css |
|
merge_main_css_into_stylesheet() { |
|
local main_css="${DV_DATA_DIR}/docroot/css/main.css" |
|
local stylesheet="${DV_DATA_DIR}/branding/custom-stylesheet.css" |
|
local marker="/* --- merged from docroot/css/main.css (sync-branding --closer) --- */" |
|
|
|
[[ -f "$main_css" ]] || return 0 |
|
[[ -f "$stylesheet" ]] || return 0 |
|
if grep -qF "$marker" "$stylesheet" 2>/dev/null; then |
|
log "main.css already merged into custom-stylesheet.css" |
|
return 0 |
|
fi |
|
log "Merging ${main_css} into custom-stylesheet.css" |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] append main.css to custom-stylesheet.css" |
|
return 0 |
|
fi |
|
{ |
|
echo "" |
|
echo "$marker" |
|
cat "$main_css" |
|
} >> "$stylesheet" |
|
} |
|
|
|
apply_closer_local_adaptations() { |
|
adapt_production_urls_in_branding |
|
merge_main_css_into_stylesheet |
|
adapt_local_header_logo |
|
} |
|
|
|
apply_closer_root_name() { |
|
local raw="${SYNC_DIR}/live-admin-settings-raw.json" |
|
local name |
|
[[ -f "$raw" ]] || return 0 |
|
[[ -n "$LOCAL_API_TOKEN" ]] || return 0 |
|
|
|
name="$(jq -r '.data[":instanceNameFull"] // empty' "$raw")" |
|
[[ -z "$name" || "$name" == "null" ]] && return 0 |
|
|
|
log "Setting root dataverse display name to: ${name}" |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] PUT ${LOCAL_URL}/api/dataverses/${ROOT_ALIAS} name=${name}" |
|
return 0 |
|
fi |
|
local body |
|
body="$(jq -n --arg name "$name" '{name: $name}')" |
|
curl -fsS -H "X-Dataverse-key: ${LOCAL_API_TOKEN}" \ |
|
-H "Content-Type: application/json" \ |
|
-X PUT "${LOCAL_URL}/api/dataverses/${ROOT_ALIAS}" \ |
|
--data-binary "$body" >/dev/null || warn "Root name PUT failed (may need fuller JSON body)" |
|
} |
|
|
|
# Local Docker has no Apache /images alias; Payara only serves /logos/* from docroot (see glassfish-web.xml) |
|
adapt_local_header_logo() { |
|
local header="${DV_DATA_DIR}/branding/custom-header.html" |
|
local logo_src="${DV_DATA_DIR}/docroot/images/libraries.logo.small.horizontal.white.cropped.png" |
|
local logo_dest_dir="${DV_DATA_DIR}/docroot/logos/navbar" |
|
local logo_dest="${logo_dest_dir}/libraries.logo.small.horizontal.white.cropped.png" |
|
local old_path="/images/libraries.logo.small.horizontal.white.cropped.png" |
|
local new_path="/logos/navbar/libraries.logo.small.horizontal.white.cropped.png" |
|
|
|
[[ -f "$header" ]] || return 0 |
|
[[ -f "$logo_src" ]] || return 0 |
|
|
|
run mkdir -p "$logo_dest_dir" |
|
if [[ ! -f "$logo_dest" ]]; then |
|
run cp "$logo_src" "$logo_dest" |
|
fi |
|
if grep -q "$old_path" "$header" 2>/dev/null; then |
|
log "Pointing custom-header logo to ${new_path} (local /logos docroot)" |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] sed ${old_path} -> ${new_path} in ${header}" |
|
else |
|
sed "s|${old_path}|${new_path}|g" "$header" > "${header}.tmp" |
|
mv "${header}.tmp" "$header" |
|
fi |
|
fi |
|
} |
|
|
|
# --- apply phase --- |
|
apply_files_from_manifest() { |
|
local entry host_path staged sync_path |
|
while IFS= read -r entry; do |
|
local isFile staged |
|
isFile="$(echo "$entry" | jq -r '.isFile')" |
|
[[ "$isFile" != "true" ]] && continue |
|
staged="$(echo "$entry" | jq -r '.stagedFile // empty')" |
|
host_path="$(echo "$entry" | jq -r '.hostPath')" |
|
[[ -z "$staged" || "$staged" == "null" ]] && continue |
|
sync_path="${SYNC_DIR}/${staged}" |
|
if [[ ! -f "$sync_path" ]]; then |
|
warn "Staged file missing, skipping copy: ${sync_path}" |
|
continue |
|
fi |
|
log " copy ${sync_path} -> ${host_path}" |
|
run mkdir -p "$(dirname "$host_path")" |
|
run cp "$sync_path" "$host_path" |
|
done < <(jq -c '.entries[]' "$MANIFEST_JSON") |
|
} |
|
|
|
apply_settings_from_manifest() { |
|
local count apply_value key local_path isFile live_val host_path |
|
count="$(jq '.entries | length' "$MANIFEST_JSON")" |
|
if [[ "$count" -eq 0 ]]; then |
|
if [[ "$(jq -r '.withTheme' "$MANIFEST_JSON")" == "true" ]]; then |
|
log "No installation branding settings to apply (theme-only or custom keys on live)." |
|
else |
|
warn "No branding settings to apply. Check live-admin-settings-raw.json or use --with-theme." |
|
fi |
|
return 0 |
|
fi |
|
|
|
if [[ "$ASSUME_YES" != true && "$DRY_RUN" != true ]]; then |
|
echo "" |
|
read -r -p "Apply ${count} branding settings to ${LOCAL_URL}? [y/N] " reply |
|
case "$reply" in |
|
y|Y|yes|YES) ;; |
|
*) die "Aborted by user" ;; |
|
esac |
|
fi |
|
|
|
log "Applying settings to ${LOCAL_URL}" |
|
|
|
while IFS= read -r entry; do |
|
key="$(echo "$entry" | jq -r '.key')" |
|
isFile="$(echo "$entry" | jq -r '.isFile')" |
|
local_path="$(echo "$entry" | jq -r '.localContainerPath')" |
|
live_val="$(echo "$entry" | jq -r '.liveValue')" |
|
host_path="$(echo "$entry" | jq -r '.hostPath')" |
|
|
|
if [[ "$isFile" == "true" ]]; then |
|
if [[ ! -f "$host_path" ]]; then |
|
warn "Skipping ${key}: file not found at ${host_path}" |
|
continue |
|
fi |
|
apply_value="$local_path" |
|
else |
|
apply_value="$live_val" |
|
fi |
|
|
|
log " PUT ${key}" |
|
curl_local_put "${LOCAL_URL}/api/admin/settings/${key}" "$apply_value" || \ |
|
warn "Failed to set ${key}" |
|
done < <(jq -c '.entries[]' "$MANIFEST_JSON") |
|
} |
|
|
|
apply_root_theme() { |
|
local theme_json="${SYNC_DIR}/root-theme.json" |
|
[[ -f "$theme_json" && -s "$theme_json" ]] || return 0 |
|
|
|
if [[ -z "$LOCAL_API_TOKEN" ]]; then |
|
warn "LOCAL_API_TOKEN not set; skipping root theme PUT (logo files may still be on disk)" |
|
return 0 |
|
fi |
|
|
|
log "Applying root collection theme via API" |
|
local body |
|
body="$(jq -n --slurpfile t "$theme_json" '{ theme: $t[0] }')" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] PUT ${LOCAL_URL}/api/dataverses/${ROOT_ALIAS}" |
|
return 0 |
|
fi |
|
|
|
if curl -fsS -H "X-Dataverse-key: ${LOCAL_API_TOKEN}" \ |
|
-H "Content-Type: application/json" \ |
|
-X PUT "${LOCAL_URL}/api/dataverses/${ROOT_ALIAS}" \ |
|
--data-binary "$body" >/dev/null; then |
|
log " root theme updated" |
|
else |
|
warn "Root theme PUT failed" |
|
fi |
|
} |
|
|
|
apply_phase() { |
|
LOCAL_URL="${LOCAL_URL%/}" |
|
if [[ ! -f "$MANIFEST_JSON" ]]; then |
|
if [[ "$DRY_RUN" == true ]]; then |
|
warn "No manifest at ${MANIFEST_JSON}; skipping apply (pull was likely --dry-run)" |
|
return 0 |
|
fi |
|
die "Missing ${MANIFEST_JSON}; run pull first (or check SYNC_DIR)" |
|
fi |
|
|
|
log "Preflight: ${LOCAL_URL}/api/info/version" |
|
if [[ "$DRY_RUN" != true ]]; then |
|
curl -fsS "${LOCAL_URL}/api/info/version" >/dev/null || \ |
|
die "Local Dataverse not reachable at ${LOCAL_URL}. Start docker compose dev stack." |
|
else |
|
echo "[dry-run] curl ${LOCAL_URL}/api/info/version" |
|
fi |
|
|
|
apply_files_from_manifest |
|
|
|
if [[ "$(jq -r '.closer // false' "$MANIFEST_JSON")" == "true" ]]; then |
|
apply_closer_local_adaptations |
|
else |
|
adapt_local_header_logo |
|
fi |
|
|
|
apply_settings_from_manifest |
|
|
|
if [[ "$(jq -r '.withTheme' "$MANIFEST_JSON")" == "true" ]]; then |
|
apply_root_theme |
|
fi |
|
|
|
if [[ "$(jq -r '.closer // false' "$MANIFEST_JSON")" == "true" ]]; then |
|
apply_closer_root_name |
|
fi |
|
|
|
verify_local_container_files |
|
} |
|
|
|
# Ensure Payara can read files at /dv/branding inside the running container |
|
verify_local_container_files() { |
|
[[ "$DRY_RUN" == true ]] && return 0 |
|
|
|
local container="${DOCKER_CONTAINER:-dev_dataverse}" |
|
local probe="/dv/branding/custom-header.html" |
|
|
|
if ! command -v docker >/dev/null 2>&1; then |
|
return 0 |
|
fi |
|
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$container"; then |
|
warn "Container '${container}' not running; could not verify ${probe} inside Docker." |
|
return 0 |
|
fi |
|
|
|
local missing=false css_probe="/dv/docroot/css/main.css" |
|
|
|
if docker exec "$container" test -r "$probe" 2>/dev/null; then |
|
log "Verified: ${probe} is readable in ${container}" |
|
else |
|
warn "Missing in container: ${probe}" |
|
missing=true |
|
fi |
|
|
|
if [[ "$(jq -r '.withDocroot // false' "$MANIFEST_JSON" 2>/dev/null)" == "true" ]] \ |
|
|| [[ "$WITH_DOCROOT" == true || "$CLOSER_MODE" == true ]]; then |
|
if docker exec "$container" test -r "$css_probe" 2>/dev/null; then |
|
log "Verified: ${css_probe} is readable in ${container}" |
|
else |
|
warn "Missing in container: ${css_probe} (main.css may be merged into custom-stylesheet.css)" |
|
fi |
|
fi |
|
|
|
if [[ "$missing" != true ]]; then |
|
return 0 |
|
fi |
|
|
|
warn "Host files under ${DV_DATA_DIR}/ are not visible inside ${container} at /dv/." |
|
if [[ -d "${DV_DATA_DIR}/branding" ]]; then |
|
log "Copying branding and docroot into ${container}..." |
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "[dry-run] docker cp ${DV_DATA_DIR}/branding ${container}:/dv/branding" |
|
echo "[dry-run] docker cp ${DV_DATA_DIR}/docroot/. ${container}:/dv/docroot/" |
|
else |
|
docker cp "${DV_DATA_DIR}/branding" "${container}:/dv/branding" |
|
run mkdir -p "${DV_DATA_DIR}/docroot" |
|
docker cp "${DV_DATA_DIR}/docroot/." "${container}:/dv/docroot/" 2>/dev/null || true |
|
if docker exec "$container" test -r "$probe" 2>/dev/null; then |
|
log "Copy succeeded: ${probe} is now readable" |
|
else |
|
warn "Copy finished but ${probe} is still missing; check Docker volume mounts." |
|
fi |
|
if [[ "$WITH_DOCROOT" == true ]] && docker exec "$container" test -r "$css_probe" 2>/dev/null; then |
|
log "Copy succeeded: ${css_probe} is now readable" |
|
fi |
|
fi |
|
return 0 |
|
fi |
|
|
|
echo "" |
|
echo " Fix (from repo root, recreates bind mount for /dv):" |
|
echo " docker compose -f docker-compose-dev.yml down" |
|
echo " docker compose -f docker-compose-dev.yml up -d" |
|
echo "" |
|
} |
|
|
|
# --- main --- |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-h|--help) usage; exit 0 ;; |
|
--no-apply) SKIP_LOCAL_APPLY=true ;; |
|
--apply-only) APPLY_ONLY=true ;; |
|
--with-theme) WITH_THEME=true ;; |
|
--with-docroot) WITH_DOCROOT=true ;; |
|
--closer) CLOSER_MODE=true; WITH_DOCROOT=true ;; |
|
--skip-analytics) SKIP_ANALYTICS=true ;; |
|
--dry-run) DRY_RUN=true ;; |
|
--yes) ASSUME_YES=true ;; |
|
*) die "Unknown option: $1 (use --help)" ;; |
|
esac |
|
shift |
|
done |
|
|
|
require_deps |
|
|
|
if [[ "$APPLY_ONLY" == true && "$SKIP_LOCAL_APPLY" == true ]]; then |
|
die "Cannot use --no-apply and --apply-only together" |
|
fi |
|
|
|
if [[ "$APPLY_ONLY" != true ]]; then |
|
pull_phase |
|
fi |
|
|
|
if [[ "$SKIP_LOCAL_APPLY" != true ]]; then |
|
apply_phase |
|
else |
|
log "Skipped local install (--no-apply). Staged under ${SYNC_DIR}" |
|
log "Run with --apply-only when ready to update ${LOCAL_URL}" |
|
fi |
|
|
|
echo "" |
|
log "Done. Staging: ${SYNC_DIR}" |
|
if [[ "$SKIP_LOCAL_APPLY" != true ]]; then |
|
log "Open ${LOCAL_URL} and hard-refresh to verify branding." |
|
fi |