diff --git a/plinth/db.py b/plinth/db.py index 285d1f84c..42654d75c 100644 --- a/plinth/db.py +++ b/plinth/db.py @@ -3,6 +3,8 @@ Common utilities to help with handling a database. """ +import pathlib +import subprocess import threading from typing import ClassVar @@ -83,3 +85,51 @@ class DBLock: # most of the significant cases where we have seen database lock issues. lock = DBLock() + + +# +# PostgreSQL utilites +# +def _run_as_postgres(command, stdin=None, stdout=None): + """Run a command as postgres user.""" + command = ['sudo', '--user', 'postgres'] + command + return subprocess.run(command, stdin=stdin, stdout=stdout, check=True) + + +def postgres_dump_database(backup_file: str, database_name: str, + database_user: str): + """Dump PostgreSQL database to a file. + + Overwrites file if it exists. Uses pg_dump utility from postgres package + (needs to be installed). + """ + backup_path = pathlib.Path(backup_file) + backup_path.parent.mkdir(parents=True, exist_ok=True) + with open(backup_path, 'w', encoding='utf-8') as file_handle: + process = _run_as_postgres(['pg_dumpall', '--roles-only'], + stdout=subprocess.PIPE) + file_handle.write(f'DROP ROLE IF EXISTS {database_user};\n') + for line in process.stdout.decode().splitlines(): + if database_user in line: + file_handle.write(line + '\n') + + with open(backup_path, 'a', encoding='utf-8') as file_handle: + _run_as_postgres( + ['pg_dump', '--create', '--clean', '--if-exists', database_name], + stdout=file_handle) + + +def postgres_restore_database(backup_file: str, database_name): + """Restore PostgreSQL database from a file. + + Drops database and recreates it. Uses pg_dump utility from postgres package + (needs to be installed). + """ + # This is needed for old backups only. New backups include 'DROP DATABASE + # IF EXISTS' and 'CREATE DATABASE' statements. + _run_as_postgres(['dropdb', database_name]) + _run_as_postgres(['createdb', database_name]) + + with open(backup_file, 'r', encoding='utf-8') as file_handle: + _run_as_postgres(['psql', '--dbname', database_name], + stdin=file_handle) diff --git a/plinth/modules/miniflux/privileged.py b/plinth/modules/miniflux/privileged.py index e08a9b5a3..31b0bd59d 100644 --- a/plinth/modules/miniflux/privileged.py +++ b/plinth/modules/miniflux/privileged.py @@ -4,13 +4,12 @@ import json import os import pathlib -import subprocess from typing import Tuple from urllib.parse import urlparse import pexpect -from plinth import action_utils +from plinth import action_utils, db from plinth.actions import privileged from plinth.utils import is_non_empty_file @@ -124,44 +123,16 @@ def _get_database_config(): } -# The following 3 methods are duplicated in tt-rss/privileged.py - - -def _run_as_postgres(command, stdin=None, stdout=None): - """Run a command as postgres user.""" - command = ['sudo', '--user', 'postgres'] + command - return subprocess.run(command, stdin=stdin, stdout=stdout, check=True) - - @privileged def dump_database(): """Dump database to file.""" config = _get_database_config() - os.makedirs(os.path.dirname(DB_BACKUP_FILE), exist_ok=True) - with open(DB_BACKUP_FILE, 'w', encoding='utf-8') as db_backup_file: - process = _run_as_postgres(['pg_dumpall', '--roles-only'], - stdout=subprocess.PIPE) - db_backup_file.write(f'DROP ROLE IF EXISTS {config["user"]};\n') - for line in process.stdout.decode().splitlines(): - if config['user'] in line: - db_backup_file.write(line + '\n') - - with open(DB_BACKUP_FILE, 'a', encoding='utf-8') as db_backup_file: - _run_as_postgres([ - 'pg_dump', '--create', '--clean', '--if-exists', config['database'] - ], stdout=db_backup_file) + db.postgres_dump_database(DB_BACKUP_FILE, config['database'], + config['user']) @privileged def restore_database(): """Restore database from file.""" config = _get_database_config() - - # This is needed for old backups only. New backups include 'DROP DATABASE - # IF EXISTS' and 'CREATE DATABASE' statements. - _run_as_postgres(['dropdb', config['database']]) - _run_as_postgres(['createdb', config['database']]) - - with open(DB_BACKUP_FILE, 'r', encoding='utf-8') as db_restore_file: - _run_as_postgres(['psql', '--dbname', config['database']], - stdin=db_restore_file) + db.postgres_restore_database(DB_BACKUP_FILE, config['database']) diff --git a/plinth/modules/ttrss/privileged.py b/plinth/modules/ttrss/privileged.py index 5af79a56f..d184327e2 100644 --- a/plinth/modules/ttrss/privileged.py +++ b/plinth/modules/ttrss/privileged.py @@ -1,12 +1,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Configure Tiny Tiny RSS.""" -import os -import subprocess - import augeas -from plinth import action_utils +from plinth import action_utils, db from plinth.actions import privileged CONFIG_FILE = '/etc/tt-rss/config.php' @@ -124,40 +121,15 @@ def enable_api_access(): def dump_database(): """Dump database to file.""" config = _get_database_config() - os.makedirs(os.path.dirname(DB_BACKUP_FILE), exist_ok=True) - with open(DB_BACKUP_FILE, 'w', encoding='utf-8') as db_backup_file: - process = _run_as_postgres(['pg_dumpall', '--roles-only'], - stdout=subprocess.PIPE) - db_backup_file.write(f'DROP ROLE IF EXISTS {config["user"]};\n') - for line in process.stdout.decode().splitlines(): - if config['user'] in line: - db_backup_file.write(line + '\n') - - with open(DB_BACKUP_FILE, 'a', encoding='utf-8') as db_backup_file: - _run_as_postgres([ - 'pg_dump', '--create', '--clean', '--if-exists', config['database'] - ], stdout=db_backup_file) + db.postgres_dump_database(DB_BACKUP_FILE, config['database'], + config['user']) @privileged def restore_database(): """Restore database from file.""" config = _get_database_config() - - # This is needed for old backups only. New backups include 'DROP DATABASE - # IF EXISTS' and 'CREATE DATABASE' statements. - _run_as_postgres(['dropdb', config['database']]) - _run_as_postgres(['createdb', config['database']]) - - with open(DB_BACKUP_FILE, 'r', encoding='utf-8') as db_restore_file: - _run_as_postgres(['psql', '--dbname', config['database']], - stdin=db_restore_file) - - -def _run_as_postgres(command, stdin=None, stdout=None): - """Run a command as postgres user.""" - command = ['sudo', '--user', 'postgres'] + command - return subprocess.run(command, stdin=stdin, stdout=stdout, check=True) + db.postgres_restore_database(DB_BACKUP_FILE, config['database']) def load_augeas():