mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-29 10:04:36 +08:00
431 lines
23 KiB
PHP
431 lines
23 KiB
PHP
<?php
|
|
|
|
namespace App\Support\DatabaseImport;
|
|
|
|
use App\Models\ServiceDatabase;
|
|
use App\Support\PostgresUser;
|
|
use InvalidArgumentException;
|
|
|
|
class DatabaseImportCommandBuilder
|
|
{
|
|
/**
|
|
* Shell helpers shared by every restore script. The backup inside the database
|
|
* container is plain or gzip-compressed (bz2, xz and zip are decompressed by
|
|
* {@see self::buildNormalizeScript()} before it arrives). Every script decides the
|
|
* backup format before it changes anything in the database.
|
|
*/
|
|
private const PRELUDE = <<<'SH'
|
|
fail() { echo "$1" >&2; exit 1; }
|
|
is_gzip() { [ "$(head -c 2 "$backup" | od -An -tx1 | tr -d ' \n')" = 1f8b ]; }
|
|
stream() { if is_gzip; then gunzip -c "$backup"; else cat "$backup"; fi; }
|
|
header() { stream | head -c "$1" | od -An -tx1 | tr -d ' \n'; }
|
|
is_tar() { [ "$(stream | head -c 262 | tail -c 5)" = ustar ]; }
|
|
is_text() { [ "$(stream | head -c 65536 | tr -d '\000' | wc -c | tr -d ' ')" = "$(stream | head -c 65536 | wc -c | tr -d ' ')" ]; }
|
|
extract_tar() { work=$(mktemp -d) && trap 'rm -rf "$work"' EXIT && stream | tar -xf - -C "$work" || fail 'The tar backup cannot be extracted.'; }
|
|
use_single_tar_member() { [ "$(find "$work" -type f | wc -l | tr -d ' ')" = 1 ] && backup=$(find "$work" -type f); }
|
|
if is_gzip; then
|
|
gunzip -t "$backup" || fail 'The gzip backup is corrupt or incomplete. Nothing was changed.'
|
|
fi
|
|
|
|
SH;
|
|
|
|
/**
|
|
* Streams an all-databases MySQL or MariaDB dump without its system databases. The mysql
|
|
* database holds the users and their passwords (user and global_priv), so restoring it
|
|
* would give the target the source passwords after its next restart. Everything from a
|
|
* switch to a system database (`-- Current Database:` or USE) up to the next switch to
|
|
* another database is dropped, as are the CREATE DATABASE statements of system databases.
|
|
*/
|
|
private const MYSQL_SYSTEM_DATABASE_FILTER = <<<'AWK'
|
|
function database(line) {
|
|
if (index(line, "`") > 0) { sub(/^[^`]*`/, "", line); sub(/`.*$/, "", line); return tolower(line) }
|
|
gsub(/\/\*[^*]*\*\//, " ", line)
|
|
line = tolower(line)
|
|
sub(/^[ \t]*(use|create[ \t]+(database|schema))[ \t]+/, "", line)
|
|
sub(/^if[ \t]+not[ \t]+exists[ \t]+/, "", line)
|
|
sub(/[ \t;].*$/, "", line)
|
|
return line
|
|
}
|
|
function is_system(name) { return name == "mysql" || name == "sys" || name == "performance_schema" || name == "information_schema" }
|
|
/^-- Current Database: `/ { skip = is_system(database($0)) }
|
|
tolower($0) ~ /^[ \t]*use[ \t]/ { skip = is_system(database($0)) }
|
|
tolower($0) ~ /^[ \t]*create[ \t]+(database|schema)[ \t]/ { if (is_system(database($0))) { next } skip = 0 }
|
|
/^\/\*!40103 SET TIME_ZONE=@OLD_TIME_ZONE/ { skip = 0 }
|
|
!skip { print }
|
|
AWK;
|
|
|
|
/**
|
|
* mysql and mariadb client commands that run a shell command, write or read a file:
|
|
* system, tee, pager, edit and source, as a long command at the start of a line or as
|
|
* a short command (\!, \T, \P, \e, \.) anywhere. An odd backslash run starts a short
|
|
* command; mysqldump writes a backslash in data as "\\" and escapes only 0, n, r, Z and
|
|
* quotes, so its rows do not match.
|
|
*/
|
|
private const MYSQL_CLIENT_COMMAND_PATTERN = '(^|[^\\\\])(\\\\\\\\)*\\\\[!TPe.]';
|
|
|
|
private const MYSQL_NAMED_CLIENT_COMMAND_PATTERN = '^[[:space:]]*(system|tee|pager|edit|source)([[:space:]]|;|$)';
|
|
|
|
/**
|
|
* Prints a PostgreSQL SQL dump without the table data of its COPY ... FROM stdin blocks.
|
|
*/
|
|
private const POSTGRES_COPY_DATA_FILTER = 'data { if ($0 == "\\\\.") data = 0; next } { print } /^COPY .* FROM stdin;$/ { data = 1 }';
|
|
|
|
/**
|
|
* @param bool $keepOwners PostgreSQL archives: restore object owners and privileges. Off by default,
|
|
* because roles from another host (for example RDS) usually do not exist here.
|
|
* @param string|null $sqliteDatabase SQLite: the database file to restore into, validated by the resource.
|
|
* @param bool $restoreMysqlUsers MySQL and MariaDB all-databases backups: also restore the system databases
|
|
* (users, passwords and privileges). Off by default, because the source
|
|
* passwords would replace the ones Coolify stores for this database.
|
|
*/
|
|
public function buildRestoreCommand(object $resource, string $path, bool $dumpAll, bool $replaceExisting = false, bool $keepOwners = false, ?string $sqliteDatabase = null, bool $restoreMysqlUsers = false): string
|
|
{
|
|
$script = match ($this->databaseType($resource)) {
|
|
'postgresql' => $this->postgresqlPrelude().($dumpAll ? $this->postgresqlDumpAll() : $this->postgresqlSingle($replaceExisting, $keepOwners)),
|
|
'mysql' => $this->mysql('mysql', 'MYSQL', $dumpAll, $restoreMysqlUsers),
|
|
'mariadb' => $this->mysql('mariadb', 'MARIADB', $dumpAll, $restoreMysqlUsers),
|
|
'mongodb' => $this->mongodb($replaceExisting),
|
|
'sqlite' => $this->sqlite($resource->databaseFilePath($sqliteDatabase)),
|
|
default => throw new InvalidArgumentException('Database import is not supported for this database type.'),
|
|
};
|
|
|
|
return 'backup='.escapeshellarg($path)."\n".self::PRELUDE.$script;
|
|
}
|
|
|
|
/**
|
|
* Prepares a backup inside the Coolify helper image. bz2, xz and single-file zip
|
|
* backups are decompressed because database images do not ship those tools;
|
|
* plain and gzip backups are moved unchanged. Exits non-zero on any failure.
|
|
*/
|
|
public function buildNormalizeScript(string $source, string $target): string
|
|
{
|
|
$source = escapeshellarg($source);
|
|
$target = escapeshellarg($target);
|
|
|
|
return <<<SH
|
|
f={$source}
|
|
out={$target}
|
|
case "\$(head -c 6 "\$f" | od -An -tx1 | tr -d ' \\n')" in
|
|
425a68*) bunzip2 -c "\$f" > "\$out" || { echo 'The bz2 backup cannot be decompressed.' >&2; exit 1; } ;;
|
|
fd377a585a00) unxz -c "\$f" > "\$out" || { echo 'The xz backup cannot be decompressed.' >&2; exit 1; } ;;
|
|
504b0304*)
|
|
d=\$(mktemp -d) && unzip -q "\$f" -d "\$d" || { echo 'The zip backup cannot be extracted.' >&2; exit 1; }
|
|
[ "\$(find "\$d" -type f | wc -l | tr -d ' ')" = 1 ] || { echo 'A zip backup must contain exactly one file.' >&2; exit 1; }
|
|
mv "\$(find "\$d" -type f)" "\$out" ;;
|
|
*) mv "\$f" "\$out" ;;
|
|
esac
|
|
SH;
|
|
}
|
|
|
|
public function buildPostgresRestoreScanScript(object $resource, string $path): ?string
|
|
{
|
|
if ($this->databaseType($resource) !== 'postgresql') {
|
|
return null;
|
|
}
|
|
|
|
$escapedPath = escapeshellarg($path);
|
|
|
|
// Token separator PostgreSQL treats as whitespace: real whitespace, a /* ... */
|
|
// block comment (also nested or holding asterisks), or a -- comment through the
|
|
// line end. The joined scan maps line ends to '~' (after it deletes every real
|
|
// '~'), so a -- comment ends at the next '~'. Comment forms also match inside
|
|
// string literals, which can only block more input, never less.
|
|
$sep = '([[:space:]]|~|/\\*.*\\*/|--[^~]*~)';
|
|
|
|
// A TO/FROM PROGRAM token pair anywhere (so it is also found inside DO blocks,
|
|
// EXECUTE strings and function bodies), a server-side large-object import or
|
|
// export, or ALTER SYSTEM. The pair is matched without the former (^|;) anchor
|
|
// and [^;]* limit, which a ';' inside a string literal or dynamic SQL bypassed.
|
|
$sqlPattern = "(from|to){$sep}+program|lo_(import|export){$sep}*\\(|alter{$sep}+system{$sep}+(set|reset)";
|
|
// psql meta-commands that can run a shell command, anywhere on a line (a meta-command
|
|
// may follow SQL without a ';'). An odd backslash run starts a meta-command; COPY data
|
|
// writes a literal backslash as "\\", so its rows do not match. Blocked: \!, output
|
|
// piped by \o/\out/\g/\gx/\w/\write, \copy ... program, \setenv and the editor commands,
|
|
// the include commands (they run a file that \o or \copy wrote), and backquotes in the
|
|
// arguments of a command. \n, \b, \v and \N are COPY escapes that psql does not know,
|
|
// and psql does not expand backquotes for unknown commands.
|
|
$psqlPattern = '(^|[^\\\\])(\\\\\\\\)*\\\\(!|(o|out|gx?|w|write)[[:space:]][^\\\\]*\||copy[[:space:]].*program|(setenv|e|edit|ef|ev|i|ir|include|include_relative)([[:space:]]|$)|([^nbv[:space:]\\\\]|[nbv][^[:space:]\\\\])[^[:space:]\\\\]*[[:space:]][^\\\\]*`)';
|
|
$escapedSqlPattern = escapeshellarg($sqlPattern);
|
|
$escapedPsqlPattern = escapeshellarg($psqlPattern);
|
|
$contents = "{ gunzip -cf {$escapedPath} 2>/dev/null || cat {$escapedPath}; }";
|
|
// Comments are no longer stripped: deleting the rest of a line on "--" also cut
|
|
// inside string literals, which let "SELECT '--'; COPY ... PROGRAM" slip through.
|
|
// A commented-out PROGRAM statement is now blocked too, which is the safer default.
|
|
// Table data of COPY ... FROM stdin blocks is not scanned: it is data, not SQL, and ordinary
|
|
// text such as "how to program" matched the patterns. pg_dump writes each block as the line
|
|
// "COPY ... FROM stdin;", the rows, and the line "\.". When awk is missing or fails, the
|
|
// scan receives a line that matches, so the restore is blocked.
|
|
$withoutCopyData = static fn (string $source): string => "{ {$source} | LC_ALL=C awk ".escapeshellarg(self::POSTGRES_COPY_DATA_FILTER)." || echo '\\! from program'; }";
|
|
$scan = static fn (string $source): string => $withoutCopyData($source)." | LC_ALL=C grep -Eiq {$escapedPsqlPattern} || ".$withoutCopyData($source)." | tr -d '~' | tr '\\n\\r' '~~' | LC_ALL=C grep -Eiq {$escapedSqlPattern}";
|
|
$customScan = $scan('pg_restore -f - "$inspect" 2>/dev/null');
|
|
$sqlScan = $scan($contents);
|
|
$blockedProgram = 'echo \'Blocked PostgreSQL restore: the backup contains a COPY ... PROGRAM, psql shell command, lo_import/lo_export, or ALTER SYSTEM pattern. If you trust this dump, restore it from the database container terminal.\'; exit 1';
|
|
$blockedInspect = 'echo \'Blocked PostgreSQL restore: unable to inspect custom archive.\'; exit 1';
|
|
|
|
return <<<SH
|
|
command -v awk >/dev/null 2>&1 || { echo 'Blocked PostgreSQL restore: awk is required to scan the backup.'; exit 1; }
|
|
header=\$({$contents} | head -c 5)
|
|
tar_magic=\$({$contents} | head -c 262 | tail -c 5)
|
|
if [ "\$header" = 'PGDMP' ] || [ "\$tar_magic" = 'ustar' ]; then
|
|
inspect=\$(mktemp)
|
|
trap 'rm -f "\$inspect"' EXIT
|
|
if ! {$contents} > "\$inspect"; then
|
|
{$blockedInspect}
|
|
fi
|
|
if ! pg_restore -l "\$inspect" >/dev/null 2>&1; then
|
|
{$blockedInspect}
|
|
fi
|
|
if {$customScan}; then
|
|
{$blockedProgram}
|
|
fi
|
|
elif {$sqlScan}; then
|
|
{$blockedProgram}
|
|
fi
|
|
SH;
|
|
}
|
|
|
|
public function buildPostgresSafetyCommand(object $resource, string $container, string $path): ?string
|
|
{
|
|
$script = $this->buildPostgresRestoreScanScript($resource, $path);
|
|
|
|
if ($script === null) {
|
|
return null;
|
|
}
|
|
|
|
return 'docker exec '.$container.' sh -c '.escapeshellarg($script);
|
|
}
|
|
|
|
public function supports(object $resource): bool
|
|
{
|
|
return in_array($this->databaseType($resource), ['postgresql', 'mysql', 'mariadb', 'mongodb', 'sqlite'], true);
|
|
}
|
|
|
|
public function databaseType(object $resource): string
|
|
{
|
|
$class = $resource->getMorphClass();
|
|
$type = ($resource instanceof ServiceDatabase || str_contains(strtolower($class), 'service'))
|
|
? strtolower($resource->databaseType())
|
|
: strtolower($class);
|
|
|
|
return match (true) {
|
|
str_contains($type, 'postgres') => 'postgresql',
|
|
str_contains($type, 'mariadb') => 'mariadb',
|
|
str_contains($type, 'mysql') => 'mysql',
|
|
str_contains($type, 'mongo') => 'mongodb',
|
|
str_contains($type, 'sqlite') => 'sqlite',
|
|
default => 'unsupported',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sets the PostgreSQL user once (see PostgresUser) and defines the checks that run after the
|
|
* backup format is accepted and before the first change. When no user variable is set,
|
|
* "postgres" is a guess, so a failed connection must stop the restore with a clear error.
|
|
*/
|
|
private function postgresqlPrelude(): string
|
|
{
|
|
return 'pg_user='.PostgresUser::shellExpression()."\n".<<<'SH'
|
|
check_connection() {
|
|
psql -U "$pg_user" -d template1 -Atc 'SELECT 1' >/dev/null 2>&1 || fail "Cannot connect to PostgreSQL as user \"$pg_user\". Set POSTGRES_USER on the database container to its superuser. Nothing was changed."
|
|
}
|
|
check_superuser() {
|
|
[ "$(psql -U "$pg_user" -d template1 -Atc 'SELECT rolsuper FROM pg_roles WHERE rolname = current_user' 2>/dev/null)" = t ] || fail "Restoring all databases needs a PostgreSQL superuser, and \"$pg_user\" is not one or cannot connect. Set POSTGRES_USER on the database container to its superuser. Nothing was changed."
|
|
}
|
|
|
|
SH;
|
|
}
|
|
|
|
/**
|
|
* pg_dump custom and tar archives are restored with pg_restore, SQL dumps with psql.
|
|
* pg_restore cannot read gzip files, so both clients receive the backup on stdin.
|
|
* SQL cannot replace single objects, so replacing recreates the target database.
|
|
* Archives skip owners and privileges unless they are kept, because a single missing
|
|
* role would roll back the whole single-transaction restore.
|
|
*/
|
|
private function postgresqlSingle(bool $replaceExisting, bool $keepOwners): string
|
|
{
|
|
// Every restore runs in one transaction, so a failure part-way rolls back and leaves the
|
|
// current data as it was, also when --clean dropped objects first.
|
|
$owners = $keepOwners ? '' : ' --no-owner --no-acl';
|
|
$clean = $replaceExisting ? ' --clean --if-exists' : '';
|
|
$suffix = bin2hex(random_bytes(16));
|
|
$sqlRestore = $replaceExisting ? "\n new=coolify_restore_new_{$suffix}\n old=coolify_restore_old_{$suffix}\n".<<<'SH'
|
|
|
|
check_connection
|
|
echo 'SQL backups cannot replace single objects. The backup is restored into a new database first; the current database is replaced only when that restore succeeds.'
|
|
if ! existing=$(printf '%s\n' "SELECT 1 FROM pg_database WHERE datname IN (:'new', :'old');" | psql -v ON_ERROR_STOP=1 -At -v new="$new" -v old="$old" -U "$pg_user" -d template1); then
|
|
fail 'The temporary database names could not be checked. Nothing was changed.'
|
|
fi
|
|
[ -z "$existing" ] || fail 'A temporary database name is already in use. Nothing was changed.'
|
|
createdb -U "$pg_user" "$new" || exit 1
|
|
if ! stream | psql -v ON_ERROR_STOP=1 --single-transaction -U "$pg_user" -d "$new"; then
|
|
PGOPTIONS='-c client_min_messages=warning' dropdb --maintenance-db=template1 -U "$pg_user" --if-exists "$new"
|
|
fail 'The SQL restore failed. The current database was not changed.'
|
|
fi
|
|
if ! printf '%s\n' "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = :'db' AND pid <> pg_backend_pid();" 'ALTER DATABASE :"db" RENAME TO :"old";' 'ALTER DATABASE :"new" RENAME TO :"db";' | psql -v ON_ERROR_STOP=1 -v db="$db" -v old="$old" -v new="$new" -U "$pg_user" -d template1 >/dev/null; then
|
|
# Undo a half-done swap: the current database keeps its name and data.
|
|
printf '%s\n' 'ALTER DATABASE :"old" RENAME TO :"db";' | psql -v db="$db" -v old="$old" -U "$pg_user" -d template1 >/dev/null 2>&1
|
|
PGOPTIONS='-c client_min_messages=warning' dropdb --maintenance-db=template1 -U "$pg_user" --if-exists "$new"
|
|
fail 'The current database is in use and could not be replaced. Nothing was changed.'
|
|
fi
|
|
PGOPTIONS='-c client_min_messages=warning' dropdb --maintenance-db=template1 -U "$pg_user" --if-exists "$old" || exit 1
|
|
SH : <<<'SH'
|
|
|
|
check_connection
|
|
stream | psql -v ON_ERROR_STOP=1 --single-transaction -U "$pg_user" -d "$db"
|
|
SH;
|
|
|
|
return <<<SH
|
|
db=\${POSTGRES_DB:-\$pg_user}
|
|
if [ "\$(stream | head -c 5)" = PGDMP ] || is_tar; then
|
|
check_connection
|
|
stream | pg_restore --exit-on-error --single-transaction{$owners}{$clean} -U "\$pg_user" -d "\$db"
|
|
elif ! is_text; then
|
|
fail 'Unsupported PostgreSQL backup format. Use a pg_dump archive (custom or tar format) or an SQL file.'
|
|
elif stream | head -c 4096 | grep -q 'PostgreSQL database cluster dump'; then
|
|
fail 'This backup contains all databases. Select "Backup contains all databases" to restore it.'
|
|
else{$sqlRestore}
|
|
fi
|
|
SH;
|
|
}
|
|
|
|
/**
|
|
* Checks the backup format before any database is dropped, then recreates the
|
|
* target database and restores archives with pg_restore and SQL with psql.
|
|
* The restore must not stop on the first error: a pg_dumpall dump always creates the
|
|
* superuser role (and creates the target database) that already exist here, and an
|
|
* archive with owners references roles this server may not have. Stopping there would
|
|
* leave the server without the databases that were just dropped.
|
|
* pg_dumpall does not create the postgres database (it expects it to exist), so it is
|
|
* recreated too when POSTGRES_DB names another database.
|
|
*/
|
|
private function postgresqlDumpAll(): string
|
|
{
|
|
return <<<'SH'
|
|
db=${POSTGRES_DB:-$pg_user}
|
|
if [ "$(stream | head -c 5)" = PGDMP ] || is_tar; then
|
|
kind=archive
|
|
stream | pg_restore -l >/dev/null 2>&1 || fail 'pg_restore cannot read this backup archive. Nothing was changed.'
|
|
elif is_text; then
|
|
kind=sql
|
|
else
|
|
fail 'Unsupported PostgreSQL backup format. Use a pg_dump archive (custom or tar format) or an SQL file. Nothing was changed.'
|
|
fi
|
|
check_superuser
|
|
psql -U "$pg_user" -d template1 -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid()" || exit 1
|
|
psql -U "$pg_user" -d template1 -t -c "SELECT datname FROM pg_database WHERE NOT datistemplate" | xargs -I {} dropdb -U "$pg_user" --maintenance-db=template1 --if-exists {} || exit 1
|
|
createdb -U "$pg_user" "$db" || exit 1
|
|
[ "$db" = postgres ] || createdb -U "$pg_user" postgres || exit 1
|
|
if [ "$kind" = archive ]; then
|
|
stream | pg_restore -U "$pg_user" -d "$db"
|
|
else
|
|
stream | psql -U "$pg_user" -d "$db"
|
|
fi
|
|
SH;
|
|
}
|
|
|
|
/**
|
|
* MySQL and MariaDB restore SQL dumps. A tar backup must wrap exactly one dump.
|
|
* The all-databases mode checks the backup before it drops any database and skips
|
|
* the system databases unless their users and privileges are restored as well.
|
|
* The client runs the dump with its client commands disabled: --sandbox on MariaDB
|
|
* (10.5.25, 10.6.18, 10.11.8, 11.4.2 and later), --commands=FALSE on MySQL (8.0.43, 8.4.6,
|
|
* 9.4.0 and later). Older clients do not know these options, so a dump with a client
|
|
* command that runs a shell command or uses a file is refused on every client.
|
|
*/
|
|
private function mysql(string $binary, string $prefix, bool $dumpAll, bool $restoreMysqlUsers = false): string
|
|
{
|
|
$commandPattern = escapeshellarg(self::MYSQL_CLIENT_COMMAND_PATTERN);
|
|
$namedCommandPattern = escapeshellarg(self::MYSQL_NAMED_CLIENT_COMMAND_PATTERN);
|
|
$preflight = <<<SH
|
|
if is_tar; then
|
|
extract_tar
|
|
use_single_tar_member || fail 'A tar backup must contain exactly one SQL dump. Nothing was changed.'
|
|
fi
|
|
is_text || fail 'Unsupported backup format. Use an SQL dump. Nothing was changed.'
|
|
if stream | LC_ALL=C grep -Eq {$commandPattern} || stream | LC_ALL=C grep -Eiq {$namedCommandPattern}; then
|
|
fail 'Blocked restore: the backup contains a {$binary} client command (system, tee, pager, edit or source). If you trust this dump, restore it from the database container terminal. Nothing was changed.'
|
|
fi
|
|
|
|
SH;
|
|
$clientSafety = <<<SH
|
|
client_safety=
|
|
if {$binary} --help 2>/dev/null | grep -q -e '--sandbox'; then
|
|
client_safety=--sandbox
|
|
elif {$binary} --help 2>/dev/null | grep -q -e '--commands'; then
|
|
client_safety=--commands=FALSE
|
|
fi
|
|
|
|
SH;
|
|
|
|
if (! $dumpAll) {
|
|
return $preflight.$clientSafety.<<<SH
|
|
if [ "$(stream | grep -c '^USE `')" -gt 1 ]; then
|
|
fail 'This backup contains more than one database. Select "Backup contains all databases" to restore it. Nothing was changed.'
|
|
fi
|
|
stream | {$binary} \$client_safety -u \${$prefix}_USER -p\${$prefix}_PASSWORD \${$prefix}_DATABASE
|
|
SH;
|
|
}
|
|
|
|
$root = "{$binary} -u root -p\${{$prefix}_ROOT_PASSWORD}";
|
|
$database = "\${{$prefix}_DATABASE:-default}";
|
|
|
|
if ($restoreMysqlUsers) {
|
|
$source = 'stream';
|
|
} else {
|
|
// Without pipefail a failing filter would feed an empty restore, so it ends the
|
|
// input with an invalid statement and the client fails instead.
|
|
$preflight .= "command -v awk >/dev/null 2>&1 || fail 'awk is required to skip the system databases of the backup. Nothing was changed.'\n";
|
|
$source = '{ stream | awk '.escapeshellarg(self::MYSQL_SYSTEM_DATABASE_FILTER)." || echo 'Coolify could not filter the system databases of the backup;'; }";
|
|
}
|
|
|
|
return $preflight.$clientSafety.<<<SH
|
|
for pid in \$({$root} -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do {$root} -e "KILL \$pid" 2>/dev/null || true; done
|
|
{$root} -N -e "SELECT CONCAT('DROP DATABASE IF EXISTS \\`',schema_name,'\\`;') FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','mysql','performance_schema','sys');" | {$root} || exit 1
|
|
{$root} -e "CREATE DATABASE IF NOT EXISTS \\`{$database}\\`;" || exit 1
|
|
{$source} | {$binary} \$client_safety -u root -p\${{$prefix}_ROOT_PASSWORD} {$database}
|
|
SH;
|
|
}
|
|
|
|
/**
|
|
* MongoDB restores mongodump archives (plain or gzip) and dump directories packed
|
|
* as tar. Replacing existing data drops each restored collection first.
|
|
*/
|
|
private function mongodb(bool $replaceExisting): string
|
|
{
|
|
$drop = $replaceExisting ? ' --drop' : '';
|
|
|
|
return <<<SH
|
|
restore() { mongorestore --authenticationDatabase=admin --username \$MONGO_INITDB_ROOT_USERNAME --password \$MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017{$drop} "\$@"; }
|
|
if is_tar; then
|
|
extract_tar
|
|
if ! use_single_tar_member; then
|
|
bson=\$(find "\$work" -type f \\( -name '*.bson' -o -name '*.bson.gz' \\) | head -n 1)
|
|
[ -n "\$bson" ] || fail 'The tar backup does not contain a mongodump directory. Nothing was changed.'
|
|
root=\$(dirname "\$(dirname "\$bson")")
|
|
if find "\$root" -type f -name '*.bson.gz' | grep -q .; then restore --gzip --dir="\$root"; else restore --dir="\$root"; fi
|
|
exit \$?
|
|
fi
|
|
fi
|
|
[ "\$(header 4)" = 6de29981 ] || fail 'Unsupported MongoDB backup format. Use a mongodump archive or a dump directory packed as tar. Single .bson files are not supported. Nothing was changed.'
|
|
if is_gzip; then restore --gzip --archive="\$backup"; else restore --archive="\$backup"; fi
|
|
SH;
|
|
}
|
|
|
|
/**
|
|
* SQLite restores a plain or gzip-compressed database file into the selected database
|
|
* file with .restore, which replaces its contents. SQLite reads an empty file (for
|
|
* example from a failed dump) as an empty database, so only real database files are restored.
|
|
*/
|
|
private function sqlite(string $file): string
|
|
{
|
|
$file = escapeshellarg($file);
|
|
|
|
return <<<SH
|
|
stream > "\$backup.db" || fail 'The backup cannot be read. Nothing was changed.'
|
|
[ "\$(head -c 15 "\$backup.db")" = 'SQLite format 3' ] || { rm -f "\$backup.db"; fail 'The backup is not a SQLite database. Nothing was changed.'; }
|
|
sqlite3 -bail {$file} '.timeout 10000' ".restore \$backup.db"; status=\$?; rm -f "\$backup.db"; exit \$status
|
|
SH;
|
|
}
|
|
}
|