397 lines
15 KiB
Bash
Executable File
397 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Dumps a PostgreSQL database from a native or dockerized PostgreSQL instance
|
|
# running on this box. This is a wrapper script to the pg_dump command.
|
|
# Uses only the TCP connection, therefore you must enable this in pg_hba file.
|
|
#
|
|
# If the PostgreSQL is dockerized you need call as a Docker manager user
|
|
# (member of the docker Linux group).
|
|
#
|
|
# Accepts few pg_dump options as well as the optional database password
|
|
# and the optional output pathname:
|
|
#
|
|
# $0 [-U dbuser] [-P dbpass] [-h dbhost] [-p dbport]
|
|
# [-C container] [-d database] [-f dumpfile ] [-F dumpformat ]
|
|
# [--acl ] [--force]
|
|
# [database (if not in -d)] [dumpfile (if not in -f)]
|
|
#
|
|
# A special -F9 option makes a PostgreSQL 9.x compatible plain text dump.
|
|
#
|
|
# Author: Kovács Zoltán <kovacs.zoltan@smartfront.hu>
|
|
# Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
|
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
|
|
# 2023-06-18 v1.0
|
|
# new: forked from the "SMARTERP_skeleton" repository.
|
|
# 2022-11-03 v0.4
|
|
# mod: May use the peer authentication as a secondary preference.
|
|
# fix: pg_restore output overwrited the previous log lines, this has been fixed.
|
|
# 2022-01-23 v0.3
|
|
# mod: More sophisticated estimate made on storage space needed.
|
|
# 2021-03-19 v0.2
|
|
# new: Option --acl (include ACLs as well) has been added.
|
|
# new: Option -F9 (PSQL 9.x compatible plain dump) has been added (native only).
|
|
# mod: Plain text dumps are compressed now.
|
|
# mod: Typos and comments.
|
|
# fix: The sbin directories has appended to the $PATH (Debian doesn't add them).
|
|
# 2020-09-17 v0.1 Initial release
|
|
|
|
# Accepted environment variables and their defaults.
|
|
#
|
|
PGCONTAINER=${PGCONTAINER-""} # Docker container's name
|
|
PGDATABASE=${PGDATABASE-""} # Database name to dump
|
|
PGDUMP=${PGDUMP-""} # Dump file pathname
|
|
PGDUMPFORMAT=${PGDUMPFORMAT-"c"} # Dump file format
|
|
PGHOST=${PGHOST:-"localhost"} # Connection parameter
|
|
PGOPTIONS=${PGOPTIONS-""} # Options to pass to pg_dump
|
|
PGPASSWORD=${PGPASSWORD-""} # Credential for the DB user
|
|
PGPORT=${PGPORT:-"5432"} # Connection parameter
|
|
PGUSER=${PGUSER:-"postgres"} # DB user for this dump
|
|
|
|
### Temporailly ignored! Need to sanitize.
|
|
PGOPTIONS=""
|
|
|
|
# Other initialisations.
|
|
#
|
|
PGDUMPACLS="--no-acl --no-owner" # Excludes the ACLs and grants.
|
|
PGDUMPFORCED="" # Dumps despite failed checks
|
|
vetodatabases="postgres template0 template1" # Technical DBs aren't to dump
|
|
|
|
# Messages.
|
|
#
|
|
MSG_ABORTED="aborted"
|
|
MSG_BADCRED="Bad credentials for MySQL"
|
|
MSG_BADDUMPPATH="Dumpfile's directory isn't writable"
|
|
MSG_BADOPT="Invalid option"
|
|
MSG_DOESNOTRUN="Doesn't run the database container"
|
|
MSG_DOCKERGRPNEED="You must be a member of the docker group."
|
|
MSG_FAILBKP="Archiver exited with error code"
|
|
MSG_FAILDB="Unable to dump the database"
|
|
MSG_FAILCONN="Failed to connect the database"
|
|
MSG_FAILSIZE="Failed to size the database"
|
|
MSG_FORCED="but forced to continue"
|
|
MSG_MISSINGDEP="Fatal: missing dependency"
|
|
MSG_NOCOMP="Fatal: missing component"
|
|
MSG_NODIRDUMP="Directory format doesn't implemented with a dockerized database."
|
|
MSG_NOSPACE="Not enough space to dump the database"
|
|
MSG_PEERAUTH="Peer authentication has been used."
|
|
|
|
MSG_USAGE="Usage: $0 [options] [database [dump_pathname|-]]\n"
|
|
MSG_USAGE+="Option:\tENVVAR:\n"
|
|
MSG_USAGE+=" -C\tPGCONTAINER\tPostgres Docker container's name\n"
|
|
MSG_USAGE+=" -d\tPGDATABASE\tPostgres database to dump ($USER)\n"
|
|
MSG_USAGE+=" -f\tPGDUMP\t\tDumpfile pathname\n"
|
|
MSG_USAGE+=" -F\tPGDUMPFORMAT\tDumpfile format ($PGDUMPFORMAT)\n"
|
|
MSG_USAGE+=" -h\tPGHOST\t\tHostname or IP to connect (localhost)\n"
|
|
MSG_USAGE+=" -p\tPGPORT\t\tTCP port to connect (5432)\n"
|
|
MSG_USAGE+=" -P\tPGPASSWORD\tPostgres password\n"
|
|
MSG_USAGE+=" -U\tPGUSER\t\tPostgres username ($PGUSER)\n"
|
|
MSG_USAGE+="--acl\t\t\tIncludes the grants and ACLs as well\n"
|
|
MSG_USAGE+="--force\t\t\tForces the operation despite the failed checks\n"
|
|
|
|
# Basic environment settings.
|
|
LANG=C
|
|
LC_ALL=C
|
|
# We need also the sbin directories.
|
|
if ! [[ "$PATH" =~ '/sbin:' ]]; then
|
|
PATH="$PATH:/usr/local/sbin:/usr/sbin:/sbin"; fi
|
|
|
|
# Getting options.
|
|
#
|
|
while getopts ":-:C:d:D:f:F:h:H:p:P:u:U:" option
|
|
do
|
|
case ${option} in
|
|
"-" )
|
|
if [ "$OPTARG" = "acl" ]; then PGDUMPACLS=""
|
|
elif [ "$OPTARG" = "force" ]; then PGDUMPFORCED="yes"
|
|
elif [ "$OPTARG" = "help" ]; then echo -e "$MSG_USAGE" >&2; exit
|
|
else echo "$MSG_BADOPT --$OPTARG" >&2; exit 1
|
|
fi
|
|
;;
|
|
"C" ) PGCONTAINER="$OPTARG" ;;
|
|
"d" | "D" ) PGDATABASE="$OPTARG" ;;
|
|
"f" ) PGDUMPFILE="$OPTARG" ;;
|
|
"F" ) PGDUMPFORMAT="$OPTARG" ;;
|
|
"h" | "H" ) PGHOST="$OPTARG" ;;
|
|
"P" ) PGPASSWORD="$OPTARG" ;;
|
|
"p" ) PGPORT="$OPTARG" ;;
|
|
"u" | "U" ) PGUSER="$OPTARG" ;;
|
|
\? )
|
|
echo "$MSG_BADOPT -$OPTARG" >&2; exit 1
|
|
;;
|
|
esac
|
|
done; shift $((OPTIND -1))
|
|
# All options have been processed.
|
|
|
|
# Checks the dependencies.
|
|
#
|
|
# Conditional dependencies (according to native or dockerized environment).
|
|
[[ -z "$PGCONTAINER" ]] \
|
|
&& additem="psql pg_dump" \
|
|
|| additem="docker"
|
|
# Common dependencies.
|
|
TR=$(which tr 2>/dev/null)
|
|
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
|
for item in basename df date dirname egrep grep gzip hostname id \
|
|
pwd sed tail tee $additem
|
|
do
|
|
if [ -n "$(which $item)" ]
|
|
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]')=$(which $item)
|
|
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
|
done
|
|
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
|
#
|
|
# An additional bugfix (use "$(which gzip)" instead of "$GZIP"):
|
|
# https://www.gnu.org/software/gzip/manual/html_node/Environment.html
|
|
GZIP=""
|
|
|
|
# Need to be root or a Docker manager user if the DB runs in a container.
|
|
#
|
|
[[ -n "$PGCONTAINER" ]] && [[ "$USER" != 'root' ]] \
|
|
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
|
|
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
|
|
|
|
# If the PostgreSQL is dockerized the container must be running.
|
|
#
|
|
[[ -n "$PGCONTAINER" ]] \
|
|
&& [[ -z "$("$DOCKER" ps -q -f name=$PGCONTAINER)" ]] \
|
|
&& echo "$MSG_DOESNOTRUN $PGCONTAINER" >&2 && exit 1
|
|
|
|
# Determines the database to dump.
|
|
#
|
|
# Lack of -d the 1st non-option parameter is the database's name.
|
|
if [ -z "$PGDATABASE" -a -n "$1" ]; then PGDATABASE="$1"; shift; fi
|
|
# The last resort is the Linux user's name.
|
|
if [ -z "$PGDATABASE" ]; then PGDATABASE="$USER"; fi
|
|
# A humble sanitization.
|
|
if [[ ! "$PGDATABASE" =~ ^([[:alnum:]]|[_])*$ ]]; then
|
|
echo -e "$MSG_USAGE" >&2; exit 1; fi
|
|
# Silently refuses the PostgreSQL internal databases.
|
|
for veto in $vetodatabases ""
|
|
do
|
|
[[ "$PGDATABASE" = "$veto" ]] && exit 0
|
|
done
|
|
# We've a database name to dump.
|
|
|
|
# Determines the output file (or maybe a target directory) and the logfile.
|
|
#
|
|
# A generated file- or directory name maybe necessary.
|
|
DUMPNAME="$PGDATABASE.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")"
|
|
# Lack of -f the next non-option parameter is the dumpfile's pathname.
|
|
if [ -z "$PGDUMPFILE" -a -n "$1" ]; then PGDUMPFILE="$1"; shift; fi
|
|
# The last resort is the generated pathname.
|
|
if [ -z "$PGDUMPFILE" ]; then PGDUMPFILE="$("$PWD")/$DUMPNAME"; fi
|
|
#
|
|
# Let's make some checks.
|
|
#
|
|
# Dumping to the STDOUT is invalid with the directory format, we'll dump
|
|
# into a newly created directory instead.
|
|
[[ "$PGDUMPFILE" = "-" ]] && [[ "$PGDUMPFORMAT" = "d" ]] \
|
|
&& PGDUMPFILE="$("$PWD")/$DUMPNAME"
|
|
# If the given pathname is an existing directory, we need append
|
|
# a generated target filename (or directory name for Fd format).
|
|
[[ -d "$PGDUMPFILE" ]] && PGDUMPFILE+="/$DUMPNAME"
|
|
#
|
|
# Here we go with the desired pathname.
|
|
#
|
|
if [ "$PGDUMPFILE" = "-" ]; then
|
|
# If '-' was given as the PGDUMPFILE, we'll write to STDOUT w/o logs.
|
|
PGDUMPFILE=""
|
|
logfile="/dev/null"
|
|
else
|
|
# We'll write and log to a directory within the filesystem.
|
|
PGDUMPDIR="$("$DIRNAME" "$PGDUMPFILE")"
|
|
# This directory must be exist and be writable.
|
|
if [ -n "$PGDUMPDIR" ] && [ ! -d "$PGDUMPDIR" -o ! -x "$PGDUMPDIR" ]; then
|
|
echo "$MSG_BADDUMPPATH: $PGDUMPDIR" >&2; exit 1
|
|
fi
|
|
# Extends the output files properly.
|
|
if [ "$PGDUMPFORMAT" = "d" ]; then
|
|
logfile="$PGDUMPFILE.log"
|
|
elif [ "$PGDUMPFORMAT" = "9" -o "$PGDUMPFORMAT" = "p" ]; then
|
|
PGDUMPFILE="${PGDUMPFILE%.sql}.sql"
|
|
logfile="${PGDUMPFILE%.sql}.log"
|
|
elif [ "$PGDUMPFORMAT" = "t" ]; then
|
|
PGDUMPFILE="${PGDUMPFILE%.tar}.tar"
|
|
logfile="${PGDUMPFILE%.tar}.log"
|
|
else
|
|
PGDUMPFILE="${PGDUMPFILE%.dmp}.dmp"
|
|
logfile="${PGDUMPFILE%.dmp}.log"
|
|
fi
|
|
fi
|
|
# We've a suitable output and log pathname (or we've to use the STDOUT w/o logs).
|
|
|
|
# Do we connect the database?
|
|
#
|
|
if [ -n "$PGCONTAINER" ]; then
|
|
# Dockerized database.
|
|
result=$("$DOCKER" exec $PGCONTAINER \
|
|
sh -c "export PGPASSWORD=\"$PGPASSWORD\"; \
|
|
psql -U \"$PGUSER\" -w -h \"$PGHOST\" -p \"$PGPORT\" \
|
|
-d \"$PGDATABASE\" -t -c \"SELECT 1\"" 2>/dev/null)
|
|
result="${result//[[:space:]]/}"
|
|
[[ $result -ne 1 ]] && echo -e "$MSG_FAILCONN." >&2 | "$TEE" -a "$logfile" && exit 1
|
|
else
|
|
# Self-hosted database.
|
|
# Preferred method: TCP with username and password.
|
|
CONNECT="-U $PGUSER -w -h $PGHOST -p $PGPORT"
|
|
export PGPASSWORD
|
|
"$PSQL" $CONNECT -d "$PGDATABASE" -c "SELECT 1" >/dev/null 2>&1; result=$?
|
|
if [[ $result -ne 0 ]]; then
|
|
# On failure we will try the peer authentication.
|
|
CONNECT=""
|
|
"$PSQL" $CONNECT -d "$PGDATABASE" -c "SELECT 1" >/dev/null 2>&1; result=$?
|
|
[[ $result -ne 0 ]] && echo -e "$MSG_FAILCONN." >&2 | "$TEE" -a "$logfile" && exit $result
|
|
# Leaves a warning about using the peer authentication.
|
|
echo -e "$MSG_PEERAUTH" >>"$logfile"
|
|
fi
|
|
# We've a valid CONNECT clause.
|
|
fi
|
|
# We've the database connect checked.
|
|
|
|
# Do we size the database?
|
|
#
|
|
dbsize=0
|
|
# It isn't relevant when we'll dump to the STDOUT.
|
|
if [ -n "$PGDUMPFILE" ]; then
|
|
# Calculates the size of the database (MB).
|
|
if [ -n "$PGCONTAINER" ]; then
|
|
# Dockerized database.
|
|
dbsize=$("$DOCKER" exec $PGCONTAINER \
|
|
sh -c "export PGPASSWORD=\"$PGPASSWORD\"; \
|
|
psql -U \"$PGUSER\" -w -h \"$PGHOST\" -p \"$PGPORT\" -d \"$PGDATABASE\" \
|
|
-t -c \"SELECT pg_database_size('$PGDATABASE');\"" 2>/dev/null)
|
|
else
|
|
# Self-hosted database.
|
|
export PGPASSWORD
|
|
dbsize=$("$PSQL" $CONNECT -d "$PGDATABASE" \
|
|
-t -c "SELECT pg_database_size('$PGDATABASE');" 2>/dev/null)
|
|
fi
|
|
# Some sanitization
|
|
dbsize="${dbsize//[[:space:]]/}"
|
|
[[ -z "$dbsize" ]] && dbsize=0
|
|
[[ ! "$dbsize" =~ ^([[:digit:]])*$ ]] && dbsize=0
|
|
# KB units
|
|
dbsize=$(( dbsize /1024 ))
|
|
# On failure aborts here, except if it had forced.
|
|
if [ $dbsize -eq 0 ]; then echo -en "$MSG_FAILSIZE" | "$TEE" -a "$logfile"
|
|
if [ "$PGDUMPFORCED" ]; then
|
|
echo " - $MSG_FORCED" | "$TEE" -a "$logfile"
|
|
else
|
|
echo " - $MSG_ABORTED" | "$TEE" -a "$logfile"; exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
# We've the database size.
|
|
|
|
# Checks the storage space available.
|
|
#
|
|
# It isn't relevant when we'll dump to the STDOUT or the database has no size.
|
|
if [ -n "$PGDUMPFILE" -a "$dbsize" -gt 0 ]; then
|
|
# Let's estimate the dump size.
|
|
dumpsize=$(( dbsize / 10 * 8 ))
|
|
# We'll estimate 1:4 ratio on compression on the fly.
|
|
[[ "$PGDUMPFORMAT" = "c" ]] && dumpsize=$(( dumpsize / 4 ))
|
|
# We'll estimate 5:4 ratio on native dump followed by the compression.
|
|
[[ "$PGDUMPFORMAT" = "9" ]] && dumpsize=$(( dumpsize / 4 * 5 ))
|
|
# Let's calculate the available space (KB units).
|
|
freespace=$("$DF" --output=avail -k "$("$DIRNAME" "$PGDUMPFILE")" | $TAIL -n1) #"
|
|
# Is it enough?
|
|
if [ $freespace -lt $dumpsize ]; then
|
|
echo -en "$MSG_NOSPACE" | "$TEE" -a "$logfile"
|
|
# On failure aborts here, except if it had forced.
|
|
if [ "$PGDUMPFORCED" ]; then
|
|
echo " - $MSG_FORCED" | "$TEE" -a "$logfile"
|
|
else
|
|
echo " - $MSG_ABORTED" | "$TEE" -a "$logfile"; exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
# We've the space checked.
|
|
|
|
# Let's dump!
|
|
# Writes the database as it was ordered, into the dump file or to the STDOUT.
|
|
#
|
|
if [ -n "$PGCONTAINER" ]; then
|
|
# Dockerized database.
|
|
if [ -z "$PGDUMPFILE" ]; then
|
|
# STDOUT
|
|
"$DOCKER" exec $PGCONTAINER \
|
|
sh -c "export PGPASSWORD=\"$PGPASSWORD\"; \
|
|
pg_dump -U \"$PGUSER\" -w -h \"$PGHOST\" -p \"$PGPORT\" \
|
|
-F$PGDUMPFORMAT $PGDUMPACLS -d \"$PGDATABASE\""
|
|
elif [ "$PGDUMPFORMAT" = "d" ]; then
|
|
# Directory format doesn't implemented with a dockerized database.
|
|
echo "$MSG_NODIRDUMP" | "$TEE" -a "$logfile"; exit 1
|
|
else
|
|
# File
|
|
"$DOCKER" exec $PGCONTAINER \
|
|
sh -c "export PGPASSWORD=\"$PGPASSWORD\"; \
|
|
pg_dump -U \"$PGUSER\" -w -h \"$PGHOST\" -p \"$PGPORT\" \
|
|
-F$PGDUMPFORMAT $PGDUMPACLS -d \"$PGDATABASE\"" \
|
|
>"$PGDUMPFILE" 2>>"$logfile"
|
|
# If it is a plain dump, compresses it.
|
|
if [ "${PGDUMPFILE##*.}" = "sql" ]; then
|
|
"$(which gzip)" "$PGDUMPFILE"
|
|
fi
|
|
fi
|
|
# This is a fake result - TODO!
|
|
result=0
|
|
else
|
|
# Self-hosted database.
|
|
export PGPASSWORD
|
|
if [ "$PGDUMPFORMAT" = "9" ]; then
|
|
# Backward-compatible SQL dump.
|
|
if [ -z "$PGDUMPFILE" ]; then
|
|
# STDOUT
|
|
# 1st the schema with some arbitrary conversions.
|
|
"$PG_DUMP" $CONNECT \
|
|
$PGDUMPACLS --schema-only -d "$PGDATABASE" | \
|
|
"$EGREP" -iv '^SET idle_in_transaction_session_timeout =' | \
|
|
"$EGREP" -iv '^SET default_table_access_method =' | \
|
|
"$SED" 's/FUNCTION =/PROCEDURE = /' | \
|
|
"$SED" "s/CURRENT_DATE/\('now'::text\)::date/g"
|
|
# 2nd the data as COPY statements.
|
|
"$PG_DUMP" $CONNECT \
|
|
$PGDUMPACLS --data-only -d "$PGDATABASE"
|
|
else
|
|
# File
|
|
# 1st the schema with some arbitrary conversions.
|
|
"$PG_DUMP" $CONNECT \
|
|
$PGDUMPACLS --schema-only -d "$PGDATABASE" | \
|
|
"$EGREP" -iv '^SET idle_in_transaction_session_timeout =' | \
|
|
"$EGREP" -iv '^SET default_table_access_method =' | \
|
|
"$SED" 's/FUNCTION =/PROCEDURE = /' | \
|
|
"$SED" "s/CURRENT_DATE/\('now'::text\)::date/g" \
|
|
>"$PGDUMPFILE" 2>>"$logfile"; result=$?
|
|
# 2nd the data as COPY statements.
|
|
"$PG_DUMP" $CONNECT \
|
|
$PGDUMPACLS --data-only -d "$PGDATABASE" \
|
|
>>"$PGDUMPFILE" 2>>"$logfile"; result=$?
|
|
# Finally compresses it.
|
|
"$(which gzip)" "$PGDUMPFILE"
|
|
fi
|
|
elif [ -z "$PGDUMPFILE" ]; then
|
|
# STDOUT
|
|
"$PG_DUMP" $CONNECT \
|
|
-F"$PGDUMPFORMAT" $PGDUMPACLS -d "$PGDATABASE" \
|
|
2>>"$logfile"; result=$?
|
|
elif [ "$PGDUMPFORMAT" = "d" ]; then
|
|
# Directory
|
|
"$PG_DUMP" $CONNECT \
|
|
-F"$PGDUMPFORMAT" $PGDUMPACLS -d "$PGDATABASE" \
|
|
-f "$PGDUMPFILE" 2>>"$logfile"; result=$?
|
|
else
|
|
# File
|
|
"$PG_DUMP" $CONNECT \
|
|
-F"$PGDUMPFORMAT" $PGDUMPACLS -d "$PGDATABASE" \
|
|
>"$PGDUMPFILE" 2>>"$logfile"; result=$?
|
|
# If it is a plain dump, compresses it.
|
|
if [ "${PGDUMPFILE##*.}" = "sql" ]; then
|
|
"$(which gzip)" "$PGDUMPFILE"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
exit $result
|
|
# That's all, Folks! :)
|