install.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/sh
# ============================================================
# FreeFrame installer: one command on a fresh Ubuntu or Debian server.
#
# sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/Techiebutler/freeframe/latest/install.sh)"
#
# This small script downloads the freeframe command from the same branch or
# tag and runs `freeframe install` from that download, with the terminal
# attached, so its questions can be answered. It writes nothing itself:
# `freeframe install` puts the command at /usr/local/bin/freeframe once its
# checks have passed and the install goes ahead, so a refused run leaves the
# server as it was.
#
# Answers can be given ahead of time. With bash -c, the word right after the
# script is only its name, so write install there and the options after it.
# sudo drops variables set before it, so put them after it:
#
# sudo bash -c "$(curl -fsSL .../install.sh)" install --domain review.yourstudio.com
# sudo FREEFRAME_CHANNEL=stable bash -c "$(curl -fsSL .../stable/install.sh)"
#
# Without a terminal (cloud-init, CI) every answer must be given ahead of
# time with --yes, and the script can then also arrive through a pipe:
#
# curl -fsSL .../install.sh | sudo FREEFRAME_ADMIN_EMAIL=you@yourstudio.com bash -s -- --yes
#
# A piped run that asks questions does not work under sudo-rs (Ubuntu
# 26.04's sudo): the questions freeze. This script then stops before it
# downloads anything and shows the bash -c form instead.
#
# The branch or tag is --channel when given, otherwise FREEFRAME_CHANNEL,
# otherwise latest.
#
# Everything sits in main(), which runs on the last line, so a download that
# is cut short never runs half a script.
# ============================================================
RAW=https://raw.githubusercontent.com/Techiebutler/freeframe
GUIDE=https://github.com/Techiebutler/freeframe/blob/main/docs/deployment.md
# quote WORD: WORD as the shell needs it typed (in single quotes when it
# holds anything but plain characters).
quote() {
case $1 in
"" | *[!A-Za-z0-9._/=:@,+-]*) printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" ;;
*) printf '%s' "$1" ;;
esac
}
# install_cmd REF [ARGS...]: the interactive install command for REF, which
# works with every sudo. FREEFRAME_CHANNEL is named for any ref but latest,
# because the script cannot see the address it was downloaded from.
# The $( ) is printed for the reader to type, never run here.
# shellcheck disable=SC2016
install_cmd() {
cmd_ref=$1
shift
if [ "$cmd_ref" = latest ]; then
printf 'sudo bash -c "$(curl -fsSL %s/%s/install.sh)"' "$RAW" "$cmd_ref"
else
printf 'sudo FREEFRAME_CHANNEL=%s bash -c "$(curl -fsSL %s/%s/install.sh)"' "$cmd_ref" "$RAW" "$cmd_ref"
fi
# Passwords and keys are never printed back (secret_options names them).
cmd_skip=""
cmd_first=1
for cmd_arg in "$@"; do
if [ -n "$cmd_skip" ]; then
cmd_skip=""
continue
fi
case $cmd_arg in
--admin-password | --s3-secret-key | --smtp-password) cmd_skip=1 && continue ;;
--admin-password=* | --s3-secret-key=* | --smtp-password=*) continue ;;
esac
if [ -n "$cmd_first" ]; then
printf ' install'
cmd_first=""
fi
printf ' %s' "$(quote "$cmd_arg")"
done
printf '\n'
}
# secret_options ARGS...: the password and key options among ARGS, one per
# line, each once. install_cmd leaves them out of the command it shows.
secret_options() {
for opt_arg in "$@"; do
case $opt_arg in
--admin-password | --admin-password=*) echo --admin-password ;;
--s3-secret-key | --s3-secret-key=*) echo --s3-secret-key ;;
--smtp-password | --smtp-password=*) echo --smtp-password ;;
esac
done | awk '!seen[$0]++'
}
# and_list WORD...: "a", "a and b", "a, b and c".
and_list() {
while [ $# -gt 2 ]; do
printf '%s, ' "$1"
shift
done
if [ $# -eq 2 ]; then printf '%s and %s' "$1" "$2"; else printf '%s' "$1"; fi
}
# secret_variable OPTION: the FREEFRAME_ variable for a password option.
secret_variable() {
printf 'FREEFRAME_%s' "$(printf '%s' "${1#--}" | tr 'a-z-' 'A-Z_')"
}
# not_yet LEAD IF: a "What to try" (LEAD, then IF, as "If it can,") for a
# channel that may not have the installer yet. On latest there is no other
# channel to suggest.
not_yet() {
if [ "$ref" = latest ]; then
echo "What to try: $1 $2 the latest release may not have the installer yet (it needs a release after v1.21.0): wait for the next release, or set FreeFrame up by hand as the guide explains: $GUIDE" >&2
else
echo "What to try: $1 $2 the '$ref' channel may not have the installer yet:" >&2
echo " sudo FREEFRAME_CHANNEL=latest bash -c \"\$(curl -fsSL $RAW/latest/install.sh)\"" >&2
fi
}
stdin_is_terminal() { [ -t 0 ]; }
# started_by_sudo_rs: 0 when sudo-rs started this script. The parent process
# says which sudo it is; when /proc cannot be read, sudo --version does.
# SUDO_USER is set only under sudo, so root's own shell never counts. Only
# the direct parent counts: measured on Ubuntu 26.04, sudo sh -c 'curl |
# bash' and a pipe typed in a sudo -i shell both read the keyboard fine;
# only curl | sudo bash freezes.
started_by_sudo_rs() {
[ -n "${SUDO_USER:-}" ] || return 1
if parent=$(readlink "/proc/$PPID/exe" 2>/dev/null) && [ -n "$parent" ]; then
case $parent in
*sudo-rs* | /usr/lib/cargo/bin/sudo) return 0 ;;
*) return 1 ;;
esac
fi
case $(sudo --version 2>/dev/null | head -n 1) in
sudo-rs*) return 0 ;;
esac
return 1
}
# sudo_rs_would_freeze ARGS...: 0 when this run would ask questions with
# the script arriving through a pipe under sudo-rs. sudo-rs cannot pass the
# keyboard on then, so the first question would freeze with nothing shown.
# A run that asks nothing (--yes, FREEFRAME_YES, --help) is fine.
sudo_rs_would_freeze() {
case ${FREEFRAME_YES:-} in
1 | [Tt]rue | TRUE | [Yy]es | YES | [Yy]) return 1 ;;
esac
for arg in "$@"; do
case $arg in
-y | --yes | -h | --help) return 1 ;;
esac
done
if stdin_is_terminal; then return 1; fi
started_by_sudo_rs
}
main() {
# Run as bash -c "$(curl ...)" WORD ARGS, WORD is $0. The documented WORD
# is install; an option there (the word left out) would be lost, so it is
# taken back (a login shell's own name, such as -bash, is not an option). A leading install word among the arguments is dropped too.
case $0 in
-- | -bash | -sh | -dash | -zsh | -ksh) ;;
-?*) set -- "$0" "$@" ;;
esac
if [ "${1:-}" = install ]; then shift; fi
ref=${FREEFRAME_CHANNEL:-latest}
# The script must come from the channel it installs: --channel wins.
prev=""
for arg in "$@"; do
case $arg in
--channel=*) ref=${arg#--channel=} ;;
*) [ "$prev" = --channel ] && ref=$arg ;;
esac
prev=$arg
done
case $ref in
"" | -* | *[!A-Za-z0-9._/-]* | *..*)
echo "The channel must be a branch or tag name such as stable or latest (it is '$ref')." >&2
echo "What to try: leave it out, or use stable or latest, for example:" >&2
echo " $(install_cmd latest)" >&2
exit 2
;;
esac
# Linux servers only. On a Mac this would otherwise only stop later, at
# bash 3.2, without saying why.
kernel=$(uname -s 2>/dev/null) || kernel=""
if [ "$kernel" != Linux ]; then
case $kernel in
Darwin) what=macOS try="on a Mac, run FreeFrame with Docker Desktop and the manual setup in the guide: $GUIDE#quick-setup" ;;
MINGW* | MSYS* | CYGWIN*) what=Windows try="on Windows, Ubuntu in WSL2 with systemd switched on works for trying FreeFrame (not for a team to rely on: its address changes when Windows restarts, and it stops when the PC sleeps). Otherwise use a Linux server, or the manual setup in the guide: $GUIDE#quick-setup" ;;
*) what=${kernel:-an unknown system} try="use a Linux server, or the manual setup in the guide: $GUIDE#quick-setup" ;;
esac
echo "The FreeFrame installer is for Linux servers: Ubuntu 22.04, 24.04 or 26.04, or Debian 12 or 13, on amd64 or arm64." >&2
echo "This computer runs $what, so nothing was changed." >&2
echo "What to try: $try" >&2
exit 1
fi
if sudo_rs_would_freeze "$@"; then
echo "This server's sudo (sudo-rs, which Ubuntu 26.04 uses) cannot pass the keyboard to a script that arrives through a pipe, so the installer's questions would freeze. Nothing was changed." >&2
echo "What to try: run the installer this way instead, which works with every sudo:" >&2
echo " $(install_cmd "$ref" "$@")" >&2
secrets=$(secret_options "$@" | tr '\n' ' ')
if [ -n "$secrets" ]; then
vars=""
for o in $secrets; do vars="$vars $(secret_variable "$o")"; done
# shellcheck disable=SC2086 # option and variable names hold no spaces
if [ "$(echo $secrets | wc -w)" -eq 1 ]; then
echo "Passwords and keys are left out of that command: type $(and_list $secrets) again yourself, or give it as $(and_list $vars) after sudo (other users of this server can see options with ps, but not variables), or leave it out and type it when the installer asks." >&2
else
echo "Passwords and keys are left out of that command: type $(and_list $secrets) again yourself, or give them as $(and_list $vars) after sudo (other users of this server can see options with ps, but not variables), or leave them out and type them when the installer asks." >&2
fi
fi
echo "To install without questions, keep the pipe and give every answer ahead of time with --yes; the guide shows how: $GUIDE" >&2
exit 1
fi
if [ "$(id -u)" != 0 ]; then
echo "Please run the installer as root." >&2
echo "What to try: put sudo in front of bash:" >&2
echo " $(install_cmd "$ref")" >&2
exit 2
fi
if ! command -v curl >/dev/null 2>&1 || ! command -v bash >/dev/null 2>&1; then
echo "This server is missing curl or bash, which the installer needs." >&2
echo "What to try: sudo apt-get install -y curl bash, then run the install command again." >&2
exit 1
fi
url="$RAW/$ref/deploy/freeframe"
tmp=$(mktemp) || exit 1
# Quiet when it works: the first thing an interactive install shows is
# the FreeFrame banner. A failure says so below.
if ! curl -fsSL --retry 3 --max-time 60 "$url" -o "$tmp"; then
rm -f "$tmp"
echo "Could not download the freeframe command from $url" >&2
not_yet "check that this server can reach raw.githubusercontent.com, then run the install command again." "If it can,"
exit 1
fi
if [ "$(head -n 1 "$tmp")" != '#!/usr/bin/env bash' ] || ! bash -n "$tmp" 2>/dev/null; then
rm -f "$tmp"
echo "What was downloaded from $url is not a complete freeframe command." >&2
not_yet "run the install command again in a few minutes." "If it keeps happening,"
exit 1
fi
# Under `curl | bash` this script's input is the download, so the
# questions read the terminal instead, when there is one.
rc=0
if (: </dev/tty) 2>/dev/null; then
bash "$tmp" install --channel "$ref" "$@" </dev/tty || rc=$?
else
bash "$tmp" install --channel "$ref" "$@" </dev/null || rc=$?
fi
rm -f "$tmp"
exit "$rc"
}
# FREEFRAME_INSTALL_SH_NO_MAIN: for the tests, which source this file to
# check the functions above.
[ -n "${FREEFRAME_INSTALL_SH_NO_MAIN:-}" ] || main "$@"