diff --git a/actions/ttrss b/actions/ttrss index 33557ff39..350969d58 100755 --- a/actions/ttrss +++ b/actions/ttrss @@ -22,6 +22,7 @@ Configuration helper for Tiny Tiny RSS. import argparse import augeas +import os import subprocess from plinth import action_utils @@ -29,6 +30,7 @@ from plinth import action_utils CONFIG_FILE = '/etc/tt-rss/config.php' DEFAULT_FILE = '/etc/default/tt-rss' DATABASE_FILE = '/etc/tt-rss/database.php' +DB_BACKUP_FILE = '/var/lib/plinth/backups-data/ttrss-database.sql' def parse_arguments(): @@ -40,6 +42,9 @@ def parse_arguments(): subparsers.add_parser('setup', help='Setup Tiny Tiny RSS configuration') subparsers.add_parser('enable', help='Enable Tiny Tiny RSS site') subparsers.add_parser('disable', help='Disable Tiny Tiny RSS site') + subparsers.add_parser('dump-database', help='Dump database to file') + subparsers.add_parser('restore-database', + help='Restore database from file') subparsers.required = True return parser.parse_args() @@ -120,6 +125,27 @@ def subcommand_disable(_): action_utils.service_disable('tt-rss') +def subcommand_dump_database(_): + """Dump database to file.""" + os.makedirs(os.path.dirname(DB_BACKUP_FILE), exist_ok=True) + with open(DB_BACKUP_FILE, 'w') as db_backup_file: + _run_as_postgres(['pg_dump', 'ttrss'], stdout=db_backup_file) + + +def subcommand_restore_database(_): + """Restore database from file.""" + _run_as_postgres(['dropdb', 'ttrss']) + _run_as_postgres(['createdb', 'ttrss']) + with open(DB_BACKUP_FILE, 'r') as db_restore_file: + _run_as_postgres(['psql', '--dbname', 'ttrss'], stdin=db_restore_file) + + +def _run_as_postgres(command, stdin=None, stdout=None): + """Run a command as postgres user.""" + command = ['sudo', '--user', 'postgres'] + command + subprocess.run(command, stdin=stdin, stdout=stdout, check=True) + + def load_augeas(): """Initialize Augeas.""" aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + diff --git a/functional_tests/features/ttrss.feature b/functional_tests/features/ttrss.feature index f3743653f..9a6fbfe37 100644 --- a/functional_tests/features/ttrss.feature +++ b/functional_tests/features/ttrss.feature @@ -15,7 +15,7 @@ # along with this program. If not, see . # -@apps @ttrss @sso +@apps @ttrss @sso @backups Feature: TT-RSS News Feed Reader Run TT-RSS News Feed Reader. @@ -28,6 +28,15 @@ Scenario: Enable ttrss application When I enable the ttrss application Then the ttrss service should be running +Scenario: Backup and restore ttrss + Given the ttrss application is enabled + And I subscribe to a feed + When I create a backup of the ttrss app data + And I unsubscribe from the feed + And I restore the ttrss app data backup + Then the ttrss service should be running + And I should be subscribed to the feed + Scenario: Disable ttrss application Given the ttrss application is enabled When I disable the ttrss application diff --git a/functional_tests/step_definitions/site.py b/functional_tests/step_definitions/site.py index fcd6603ec..abbf40b49 100644 --- a/functional_tests/step_definitions/site.py +++ b/functional_tests/step_definitions/site.py @@ -240,3 +240,18 @@ def syncthing_assert_folder_present(browser, folder_name): @then(parsers.parse('syncthing folder {folder_name:w} should not be present')) def syncthing_assert_folder_not_present(browser, folder_name): assert not site.syncthing_folder_is_present(browser, folder_name) + + +@given('I subscribe to a feed') +def ttrss_subscribe(browser): + site.ttrss_subscribe(browser) + + +@when('I unsubscribe from the feed') +def ttrss_unsubscribe(browser): + site.ttrss_unsubscribe(browser) + + +@then('I should be subscribed to the feed') +def ttrss_assert_subscribed(browser): + assert site.ttrss_is_subscribed(browser) diff --git a/functional_tests/support/site.py b/functional_tests/support/site.py index b16725be8..8ee20f370 100644 --- a/functional_tests/support/site.py +++ b/functional_tests/support/site.py @@ -578,3 +578,53 @@ def syncthing_remove_folder(browser, folder_name): remove_folder_dialog.find_by_xpath(remove_button_xpath).first.click() eventually(lambda: not folder_dialog.visible) + + +def _ttrss_load_main_interface(browser): + """Load the TT-RSS interface.""" + access_url(browser, 'tt-rss') + overlay = browser.find_by_id('overlay') + eventually(lambda: not overlay.visible) + + +def _ttrss_is_feed_shown(browser, invert=False): + return browser.is_text_present('Planet Debian') != invert + + +def ttrss_subscribe(browser): + """Subscribe to a feed in TT-RSS.""" + _ttrss_load_main_interface(browser) + browser.find_by_text('Actions...').click() + browser.find_by_text('Subscribe to feed...').click() + browser.find_by_id( + 'feedDlg_feedUrl').fill('https://planet.debian.org/atom.xml') + browser.find_by_text('Subscribe').click() + if browser.is_text_present('You are already subscribed to this feed.'): + browser.find_by_text('Cancel').click() + + expand = browser.find_by_css('span.dijitTreeExpandoClosed') + if expand: + expand.first.click() + + assert eventually(_ttrss_is_feed_shown, [browser]) + + +def ttrss_unsubscribe(browser): + """Unsubscribe from a feed in TT-RSS.""" + _ttrss_load_main_interface(browser) + expand = browser.find_by_css('span.dijitTreeExpandoClosed') + if expand: + expand.first.click() + + browser.find_by_text('Planet Debian').click() + browser.execute_script("quickMenuGo('qmcRemoveFeed')") + prompt = browser.get_alert() + prompt.accept() + + assert eventually(_ttrss_is_feed_shown, [browser, True]) + + +def ttrss_is_subscribed(browser): + """Return whether subscribed to a feed in TT-RSS.""" + _ttrss_load_main_interface(browser) + return browser.is_text_present('Planet Debian') diff --git a/plinth/modules/ttrss/__init__.py b/plinth/modules/ttrss/__init__.py index ef7895c82..7ec11afd0 100644 --- a/plinth/modules/ttrss/__init__.py +++ b/plinth/modules/ttrss/__init__.py @@ -27,7 +27,7 @@ from plinth.menu import main_menu from plinth.modules.users import register_group from plinth.utils import format_lazy -from .manifest import clients +from .manifest import backup, clients version = 3 @@ -132,3 +132,13 @@ def diagnose(): check_certificate=False)) return results + + +def backup_pre(packet): + """Save database contents.""" + actions.superuser_run('ttrss', ['dump-database']) + + +def restore_post(packet): + """Restore database contents.""" + actions.superuser_run('ttrss', ['restore-database']) diff --git a/plinth/modules/ttrss/manifest.py b/plinth/modules/ttrss/manifest.py index 4fac04f53..b12c609c9 100644 --- a/plinth/modules/ttrss/manifest.py +++ b/plinth/modules/ttrss/manifest.py @@ -17,6 +17,7 @@ from django.utils.translation import ugettext_lazy as _ +from plinth.modules.backups.api import validate as validate_backup from plinth.clients import store_url, validate clients = validate([{ @@ -41,3 +42,13 @@ clients = validate([{ 'url': '/tt-rss' }] }]) + +backup = validate_backup({ + 'data': { + 'files': ['/var/lib/plinth/backups-data/ttrss-database.sql'] + }, + 'secrets': { + 'files': ['/etc/tt-rss/database.php'] + }, + 'services': ['tt-rss'] +})