From f0bb83654de31d277be7796d5617ccc515c1e8a6 Mon Sep 17 00:00:00 2001 From: "V.G. Bulavintsev" Date: Tue, 16 Nov 2021 16:59:41 +0100 Subject: [PATCH 1/2] Remove obsolete DB and config upgrade support --- .../implementation/config_converter.py | 119 +--- .../upgrade/implementation/legacy_to_pony.py | 510 ------------------ .../upgrade/implementation/tests/conftest.py | 0 .../tests/test_config_upgrade_to_74.py | 109 ---- .../tests/test_config_upgrade_to_75.py | 35 -- .../tests/test_legacy_to_pony.py | 189 ------- .../implementation/tests/test_upgrader.py | 88 +-- .../upgrade/implementation/upgrade.py | 118 +--- src/tribler-core/tribler_core/conftest.py | 9 - .../{pony_v6.db => pony_v8.db} | Bin 233472 -> 229376 bytes .../data/upgrade_databases/tribler_v29.sdb | Bin 598016 -> 0 bytes 11 files changed, 14 insertions(+), 1163 deletions(-) delete mode 100644 src/tribler-core/tribler_core/components/upgrade/implementation/legacy_to_pony.py delete mode 100644 src/tribler-core/tribler_core/components/upgrade/implementation/tests/conftest.py delete mode 100644 src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_config_upgrade_to_74.py delete mode 100644 src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_config_upgrade_to_75.py delete mode 100644 src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_legacy_to_pony.py rename src/tribler-core/tribler_core/tests/tools/data/upgrade_databases/{pony_v6.db => pony_v8.db} (79%) delete mode 100644 src/tribler-core/tribler_core/tests/tools/data/upgrade_databases/tribler_v29.sdb diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/config_converter.py b/src/tribler-core/tribler_core/components/upgrade/implementation/config_converter.py index 6a6330c63e7..b03ea8c8711 100644 --- a/src/tribler-core/tribler_core/components/upgrade/implementation/config_converter.py +++ b/src/tribler-core/tribler_core/components/upgrade/implementation/config_converter.py @@ -1,127 +1,10 @@ -import ast -import base64 import logging -import os -from configparser import MissingSectionHeaderError -from lib2to3.pgen2.parse import ParseError -from configobj import ConfigObj, ParseError as ConfigObjParseError - -from tribler_common.simpledefs import STATEDIR_CHECKPOINT_DIR - -from tribler_core.components.libtorrent.download_manager.download_config import DownloadConfig -from tribler_core.components.libtorrent.torrentdef import TorrentDef -from tribler_core.utilities.configparser import CallbackConfigParser -from tribler_core.components.libtorrent.utils.libtorrent_helper import libtorrent as lt -from tribler_core.utilities.unicode import recursive_ungarble_metainfo +from configobj import ConfigObj logger = logging.getLogger(__name__) -def load_config(filename: str): - return ConfigObj(infile=filename, encoding='utf8') - - -def convert_config_to_tribler74(state_dir): - """ - Convert the download config files to Tribler 7.4 format. The extensions will also be renamed from .state to .conf - """ - from lib2to3.refactor import RefactoringTool, get_fixers_from_package - refactoring_tool = RefactoringTool(fixer_names=get_fixers_from_package('lib2to3.fixes')) - - for filename in (state_dir / STATEDIR_CHECKPOINT_DIR).glob('*.state'): - convert_state_file_to_conf_74(filename, refactoring_tool=refactoring_tool) - - -def convert_state_file_to_conf_74(filename, refactoring_tool=None): - """ - Converts .pstate file (pre-7.4.0) to .conf file. - :param filename: .pstate file - :param refactoring_tool: RefactoringTool instance if using Python3 - :return: fixed config - """ - def _fix_state_config(config): - for section, option in [('state', 'metainfo'), ('state', 'engineresumedata')]: - value = config.get(section, option, literal_eval=False) - if not value or not refactoring_tool: - continue - - try: - value = str(refactoring_tool.refactor_string(value + '\n', option + '_2to3')) - ungarbled_dict = recursive_ungarble_metainfo(ast.literal_eval(value)) - value = ungarbled_dict or ast.literal_eval(value) - config.set(section, option, base64.b64encode(lt.bencode(value)).decode('utf-8')) - except (ValueError, SyntaxError, ParseError) as ex: - logger.error("Config could not be fixed, probably corrupted. Exception: %s %s", type(ex), str(ex)) - return None - return config - - old_config = CallbackConfigParser() - try: - old_config.read_file(str(filename)) - except MissingSectionHeaderError: - logger.error("Removing download state file %s since it appears to be corrupt", filename) - os.remove(filename) - - # We first need to fix the .state file such that it has the correct metainfo/resumedata. - # If the config cannot be fixed, it is likely corrupted in which case we simply remove the file. - fixed_config = _fix_state_config(old_config) - if not fixed_config: - logger.error("Removing download state file %s since it could not be fixed", filename) - os.remove(filename) - return - - # Remove dlstate since the same information is already stored in the resumedata - if old_config.has_option('state', 'dlstate'): - old_config.remove_option('state', 'dlstate') - - try: - conf_filename = str(filename)[:-6] + '.conf' - new_config = load_config(conf_filename) - for section in old_config.sections(): - for key, _ in old_config.items(section): - val = old_config.get(section, key) - if section not in new_config: - new_config[section] = {} - new_config[section][key] = val - new_config.write() - os.remove(filename) - except ConfigObjParseError: - logger.error("Could not parse %s file on upgrade so removing it", filename) - os.remove(filename) - - return fixed_config - - -def convert_config_to_tribler75(state_dir): - """ - Convert the download config files from Tribler 7.4 to 7.5 format. - """ - for filename in (state_dir / STATEDIR_CHECKPOINT_DIR).glob('*.conf'): - try: - config = DownloadConfig.load(filename) - - # Convert resume data - resumedata = config.get_engineresumedata() - if b'mapped_files' in resumedata: - resumedata.pop(b'mapped_files') - config.set_engineresumedata(resumedata) - config.write(str(filename)) - - # Convert metainfo - metainfo = config.get_metainfo() - if not config.config['download_defaults'].get('selected_files') or not metainfo: - # no conversion needed/possible, selected files will be reset to their default (i.e., all files) - continue - tdef = TorrentDef.load_from_dict(metainfo) - config.set_selected_files([tdef.get_index_of_file_in_files(fn) - for fn in config.config['download_defaults'].pop('selected_files')]) - config.write(str(filename)) - except (ConfigObjParseError, UnicodeDecodeError): - logger.error("Could not parse %s file so removing it", filename) - os.remove(filename) - - def convert_config_to_tribler76(state_dir): """ Convert the download config files from Tribler 7.5 to 7.6 format. diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/legacy_to_pony.py b/src/tribler-core/tribler_core/components/upgrade/implementation/legacy_to_pony.py deleted file mode 100644 index 6ae91b03af5..00000000000 --- a/src/tribler-core/tribler_core/components/upgrade/implementation/legacy_to_pony.py +++ /dev/null @@ -1,510 +0,0 @@ -import base64 -import contextlib -import datetime -import logging -import os -import sqlite3 -from asyncio import sleep - -from pony import orm -from pony.orm import db_session - -from tribler_core.components.metadata_store.category_filter.l2_filter import is_forbidden -from tribler_core.components.metadata_store.db.orm_bindings.channel_metadata import BLOB_EXTENSION -from tribler_core.components.metadata_store.db.orm_bindings.channel_node import LEGACY_ENTRY, NEW -from tribler_core.components.metadata_store.db.orm_bindings.torrent_metadata import infohash_to_id -from tribler_core.components.metadata_store.db.serialization import REGULAR_TORRENT, int2time, time2int -from tribler_core.components.metadata_store.db.store import BETA_DB_VERSIONS, CURRENT_DB_VERSION -from tribler_core.utilities.path_util import Path -from tribler_core.utilities.tracker_utils import get_uniformed_tracker_url - -DISCOVERED_CONVERSION_STARTED = "discovered_conversion_started" -CHANNELS_CONVERSION_STARTED = "channels_conversion_started" -TRACKERS_CONVERSION_STARTED = "trackers_conversion_started" -PERSONAL_CONVERSION_STARTED = "personal_conversion_started" - -CONVERSION_STARTED = "conversion_started" -CONVERSION_FINISHED = "conversion_finished" - -CONVERSION_FROM_72 = "conversion_from_72" -CONVERSION_FROM_72_PERSONAL = "conversion_from_72_personal" -CONVERSION_FROM_72_DISCOVERED = "conversion_from_72_discovered" -CONVERSION_FROM_72_CHANNELS = "conversion_from_72_channels" - - -def final_timestamp(): - return 1 << 62 - - -class DispersyToPonyMigration: - select_channels_sql = ( - "SELECT id, name, dispersy_cid, modified, nr_torrents, nr_favorite, nr_spam " - "FROM Channels " - "WHERE nr_torrents >= 3 " - "AND name not NULL;" - ) - select_trackers_sql = "SELECT tracker_id, tracker, last_check, failures, is_alive FROM TrackerInfo" - - select_full = ( - "SELECT" - " (SELECT ti.tracker FROM TorrentTrackerMapping ttm, TrackerInfo ti WHERE " - "ttm.torrent_id == t.torrent_id AND ttm.tracker_id == ti.tracker_id AND ti.tracker != 'DHT' " - "AND ti.tracker != 'http://retracker.local/announce' ORDER BY ti.is_alive ASC, ti.failures DESC, " - "ti.last_check ASC), chs.dispersy_cid, ct.name, t.infohash, t.length, t.creation_date, " - "t.torrent_id, t.category, t.num_seeders, t.num_leechers, t.last_tracker_check " - "FROM _ChannelTorrents ct, Torrent t, Channels chs WHERE ct.name NOT NULL and t.length > 0 AND " - "t.category NOT NULL AND ct.deleted_at IS NULL AND t.torrent_id == ct.torrent_id AND " - "t.infohash NOT NULL AND ct.channel_id == chs.id" - ) - select_torrents_sql = ( - "FROM _ChannelTorrents ct, Torrent t WHERE " - "ct.name NOT NULL and t.length>0 AND t.category NOT NULL AND ct.deleted_at IS NULL " - " AND t.torrent_id == ct.torrent_id AND t.infohash NOT NULL" - ) - - def __init__(self, tribler_db, notifier_callback=None, logger=None): - self._logger = logger or logging.getLogger(self.__class__.__name__) - self.notifier_callback = notifier_callback - self.tribler_db = tribler_db - self.mds = None - self.shutting_down = False - self.conversion_start_timestamp_int = time2int(datetime.datetime.utcnow()) - - self.personal_channel_id = None - self.personal_channel_title = None - - def initialize(self, mds): - self.mds = mds - try: - self.personal_channel_id, self.personal_channel_title = self.get_personal_channel_id_title() - self.personal_channel_title = self.personal_channel_title[:200] # limit the title size - except: - self._logger.info("No personal channel found") - - def get_old_channels(self): - with contextlib.closing(sqlite3.connect(self.tribler_db)) as connection, connection: - cursor = connection.cursor() - - channels = [] - for id_, name, dispersy_cid, modified, nr_torrents, nr_favorite, nr_spam in cursor.execute( - self.select_channels_sql): - if nr_torrents and nr_torrents > 0: - channels.append({"id_": infohash_to_id(dispersy_cid), - "infohash": bytes(dispersy_cid), - "title": name or '', - "public_key": b"", - "timestamp": final_timestamp(), - "origin_id": 0, - "size": 0, - "subscribed": False, - "status": LEGACY_ENTRY, - "votes": -1, - "num_entries": int(nr_torrents or 0)}) - return channels - - def get_personal_channel_id_title(self): - with contextlib.closing(sqlite3.connect(self.tribler_db)) as connection: - cursor = connection.cursor() - cursor.execute('SELECT id,name FROM Channels WHERE peer_id ISNULL LIMIT 1') - result = cursor.fetchone() - return result - - def get_old_trackers(self): - with contextlib.closing(sqlite3.connect(self.tribler_db)) as connection, connection: - cursor = connection.cursor() - - trackers = {} - for tracker_id, tracker, last_check, failures, is_alive in cursor.execute(self.select_trackers_sql): - try: - tracker_url_sanitized = get_uniformed_tracker_url(tracker) - if not tracker_url_sanitized: - continue - except Exception as e: - self._logger.warning("Encountered malformed tracker: %s", e) - # Skip malformed trackers - continue - trackers[tracker_url_sanitized] = ({ - "last_check": last_check, - "failures": failures, - "alive": is_alive}) - return trackers - - def get_old_torrents_count(self, personal_channel_only=False): - personal_channel_filter = "" - if self.personal_channel_id: - equality_sign = " == " if personal_channel_only else " != " - personal_channel_filter = f"AND ct.channel_id {equality_sign} {self.personal_channel_id}" - - command = ( - f"SELECT COUNT(*) FROM (SELECT t.torrent_id {self.select_torrents_sql} {personal_channel_filter} " - "group by infohash )" - ) - with contextlib.closing(sqlite3.connect(self.tribler_db)) as connection, connection: - cursor = connection.cursor() - cursor.execute(command) - result = cursor.fetchone()[0] - return result - - def get_personal_channel_torrents_count(self): - command = ( - f"SELECT COUNT(*) FROM (SELECT t.torrent_id {self.select_torrents_sql} " - f"AND ct.channel_id == {self.personal_channel_id} group by infohash )" - ) - with contextlib.closing(sqlite3.connect(self.tribler_db)) as connection, connection: - cursor = connection.cursor() - cursor.execute(command) - result = cursor.fetchone()[0] - return result - - def get_old_torrents(self, personal_channel_only=False, batch_size=10000, offset=0, - sign=False): - with contextlib.closing(sqlite3.connect(self.tribler_db)) as connection, connection: - cursor = connection.cursor() - cursor.execute("PRAGMA temp_store = 2") - - personal_channel_filter = "" - if self.personal_channel_id: - equality_sign = " == " if personal_channel_only else " != " - personal_channel_filter = f"AND ct.channel_id {equality_sign} {self.personal_channel_id}" - - torrents = [] - batch_not_empty = False # This is a dumb way to indicate that this batch got zero entries from DB - - for tracker_url, channel_id, name, infohash, length, creation_date, torrent_id, category, num_seeders, \ - num_leechers, last_tracker_check in cursor.execute( - f"{self.select_full} {personal_channel_filter} group by infohash " - f"LIMIT {batch_size} OFFSET {offset}" - ): - batch_not_empty = True - # check if name is valid unicode data - try: - name = str(name) - except UnicodeDecodeError: - continue - - try: - invalid_decoding = len(base64.decodebytes(infohash.encode('utf-8'))) != 20 - invalid_id = not torrent_id or int(torrent_id) == 0 - invalid_length = not length or (int(length) <= 0) or (int(length) > (1 << 45)) - invalid_name = not name or is_forbidden(name) - if invalid_decoding or invalid_id or invalid_length or invalid_name: - continue - - infohash = base64.decodebytes(infohash.encode()) - - torrent_date = datetime.datetime.utcfromtimestamp(creation_date or 0) - torrent_date = torrent_date if 0 <= time2int(torrent_date) <= self.conversion_start_timestamp_int \ - else int2time(0) - torrent_dict = { - "status": NEW, - "infohash": infohash, - "size": int(length), - "torrent_date": torrent_date, - "title": name or '', - "tags": category or '', - "tracker_info": tracker_url or '', - "xxx": int(category == 'xxx')} - if not sign: - torrent_dict.update({"origin_id": infohash_to_id(channel_id)}) - seeders = int(num_seeders or 0) - leechers = int(num_leechers or 0) - last_tracker_check = int(last_tracker_check or 0) - health_dict = { - "seeders": seeders, - "leechers": leechers, - "last_check": last_tracker_check - } if (last_tracker_check >= 0 and seeders >= 0 and leechers >= 0) else None - torrents.append((torrent_dict, health_dict)) - except Exception as e: - self._logger.warning("During retrieval of old torrents an exception was raised: %s", e) - continue - - return torrents if batch_not_empty else None - - async def convert_personal_channel(self): - with db_session: - # Reflect conversion state - v = self.mds.MiscData.get_for_update(name=CONVERSION_FROM_72_PERSONAL) - if v: - if v.value == CONVERSION_STARTED: - # Just drop the entries from the previous try - my_channels = self.mds.ChannelMetadata.get_my_channels() - for my_channel in my_channels: - my_channel.contents.delete(bulk=True) - my_channel.delete() - else: - # Something is wrong, this should never happen - raise Exception("Previous conversion resulted in invalid state") - else: - self.mds.MiscData(name=CONVERSION_FROM_72_PERSONAL, value=CONVERSION_STARTED) - my_channels_count = self.mds.ChannelMetadata.get_my_channels().count() - - # Make sure every precondition is met - if self.personal_channel_id and not my_channels_count: - total_to_convert = self.get_personal_channel_torrents_count() - - with db_session: - my_channel = self.mds.ChannelMetadata.create_channel(title=self.personal_channel_title, description='') - - def get_old_stuff(batch_size, offset): - return self.get_old_torrents(personal_channel_only=True, sign=True, - batch_size=batch_size, offset=offset) - - def add_to_pony(t): - return self.mds.TorrentMetadata(origin_id=my_channel.id_, **t) - - await self.convert_async(add_to_pony, get_old_stuff, total_to_convert, - offset=0, message="Converting personal channel torrents.") - - with db_session: - my_channel = self.mds.ChannelMetadata.get_my_channels().first() - folder = Path(my_channel._channels_dir) / my_channel.dirname - - # We check if we need to re-create the channel dir in case it was deleted for some reason - if not folder.is_dir(): - os.makedirs(Path.fix_win_long_file(folder)) - for filename in os.listdir(folder): - file_path = folder / filename - # We only remove mdblobs and leave the rest as it is - if filename.endswith(BLOB_EXTENSION) or filename.endswith(BLOB_EXTENSION + '.lz4'): - os.unlink(Path.fix_win_long_file(file_path)) - my_channel.commit_channel_torrent() - - with db_session: - v = self.mds.MiscData.get_for_update(name=CONVERSION_FROM_72_PERSONAL) - v.value = CONVERSION_FINISHED - - async def update_convert_total(self, amount, elapsed): - if self.notifier_callback: - elapsed = 0.0001 if elapsed == 0.0 else elapsed - self.notifier_callback( - f"{amount} entries converted in {int(elapsed)} seconds ({amount // elapsed} e/s)" - ) - await sleep(0.001) - - async def update_convert_progress(self, amount, total, elapsed, message=""): - if self.notifier_callback: - elapsed = 0.0001 if elapsed == 0.0 else elapsed - amount = amount or 1 - est_speed = amount / elapsed - eta = str(datetime.timedelta(seconds=int((total - amount) / est_speed))) - self.notifier_callback( - f"{message}\nConverted: {amount}/{total} ({(amount * 100) // total}).\nTime remaining: {eta}" - ) - await sleep(0.001) - - async def convert_async(self, add_to_pony, get_old_stuff, total_to_convert, offset=0, message=""): - """ - This method converts old stuff into the pony database splitting the process into chunks dynamically. - Chunks splitting uses congestion-control-like algorithm. Yields are necessary so the - reactor can get an opportunity at serving other tasks, such as sending progress notifications to - the GUI through the REST API. - This method is made semi-general, so it is possible to use it as a wrapper for actual conversion - routines for both personal and non-personal channels. - """ - start_time = datetime.datetime.utcnow() - batch_size = 100 - - reference_timedelta = datetime.timedelta(milliseconds=1000) - start = 0 + offset - elapsed = 1 - while start < total_to_convert: - batch = get_old_stuff(batch_size=batch_size, offset=start) - - if batch is None or self.shutting_down: - break - - end = start + batch_size - - batch_start_time = datetime.datetime.now() - with db_session: - for (torrent_dict, health) in batch: - try: - torrent = add_to_pony(torrent_dict) - if torrent and health: - torrent.health.set(**health) - except: - self._logger.warning("Error while converting torrent entry: %s %s", torrent_dict, health) - batch_end_time = datetime.datetime.now() - batch_start_time - - elapsed = (datetime.datetime.utcnow() - start_time).total_seconds() - await self.update_convert_progress(start, total_to_convert, elapsed, message) - target_coeff = (batch_end_time.total_seconds() / reference_timedelta.total_seconds()) - if len(batch) == batch_size: - # Adjust batch size only for full batches - if target_coeff < 0.8: - batch_size += batch_size - elif target_coeff > 1.1: - batch_size = int(float(batch_size) / target_coeff) - # we want to guarantee that at least some entries will go through - batch_size = batch_size if batch_size > 10 else 10 - self._logger.info("Converted: %i/%i %f ", - start + batch_size, total_to_convert, float(batch_end_time.total_seconds())) - start = end - - await self.update_convert_total(start, elapsed) - - async def convert_discovered_torrents(self): - offset = 0 - # Reflect conversion state - with db_session: - v = self.mds.MiscData.get_for_update(name=CONVERSION_FROM_72_DISCOVERED) - if v: - offset = orm.count( - g for g in self.mds.TorrentMetadata if - g.status == LEGACY_ENTRY and g.metadata_type == REGULAR_TORRENT) - v.set(value=CONVERSION_STARTED) - else: - self.mds.MiscData(name=CONVERSION_FROM_72_DISCOVERED, value=CONVERSION_STARTED) - - await self.convert_async(self.mds.TorrentMetadata.add_ffa_from_dict, - self.get_old_torrents, - self.get_old_torrents_count(), - offset=offset, - message="Converting old torrents.") - - with db_session: - v = self.mds.MiscData.get_for_update(name=CONVERSION_FROM_72_DISCOVERED) - v.value = CONVERSION_FINISHED - - def convert_discovered_channels(self): - # Reflect conversion state - with db_session: - v = self.mds.MiscData.get_for_update(name=CONVERSION_FROM_72_CHANNELS) - if v: - if v.value == CONVERSION_STARTED: - # Just drop the entries from the previous try - orm.delete(g for g in self.mds.ChannelMetadata if g.status == LEGACY_ENTRY) - else: - v.set(value=CONVERSION_STARTED) - else: - self.mds.MiscData(name=CONVERSION_FROM_72_CHANNELS, value=CONVERSION_STARTED) - - old_channels = self.get_old_channels() - # We break it up into separate sessions and add sleep because this is going to be executed - # on a background thread and we do not want to hold the DB lock for too long - with db_session: - for c in old_channels: - if self.shutting_down: - break - try: - self.mds.ChannelMetadata(**c) - except: - continue - - with db_session: - for c in self.mds.ChannelMetadata.select().for_update()[:]: - contents_len = c.contents_len - title = c.title - if is_forbidden(title): - c.delete() - elif contents_len: - c.num_entries = contents_len - else: - c.delete() - - with db_session: - v = self.mds.MiscData.get_for_update(name=CONVERSION_FROM_72_CHANNELS) - v.value = CONVERSION_FINISHED - - def update_trackers_info(self): - old_trackers = self.get_old_trackers() - with db_session: - trackers = self.mds.TrackerState.select().for_update()[:] - for tracker in trackers: - if tracker.url in old_trackers: - tracker.set(**old_trackers[tracker.url]) - - def mark_conversion_finished(self): - with db_session: - v = self.mds.MiscData.get(name=CONVERSION_FROM_72) - if v: - v.set(value=CONVERSION_FINISHED) - else: - self.mds.MiscData(name=CONVERSION_FROM_72, value=CONVERSION_FINISHED) - - async def do_migration(self): - await self.convert_personal_channel() - self.mds.clock = None # We should never touch the clock during legacy conversions - await self.convert_discovered_torrents() - self.convert_discovered_channels() - self.update_trackers_info() - self.mark_conversion_finished() - - -def old_db_version_ok(old_database_path): - # Check the old DB version - with contextlib.closing(sqlite3.connect(old_database_path)) as connection, connection: - cursor = connection.cursor() - cursor.execute('SELECT value FROM MyInfo WHERE entry == "version"') - version = int(cursor.fetchone()[0]) - return True if version == 29 else False - - -def cleanup_pony_experimental_db(new_database_path): - # Check for the old experimental version database - # ACHTUNG!!! NUCLEAR OPTION!!! DO NOT MESS WITH IT!!! - with contextlib.closing(sqlite3.connect(new_database_path)) as connection, connection: - cursor = connection.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'MiscData'") - result = cursor.fetchone() - delete_old_pony_db = not bool(result[0] if result else False) - if not delete_old_pony_db: - cursor.execute('SELECT value FROM MiscData WHERE name == "db_version"') - version = int(cursor.fetchone()[0]) - delete_old_pony_db = version in BETA_DB_VERSIONS # Delete the older betas DB - # We're looking at the old experimental version database. Delete it. - if delete_old_pony_db: - os.unlink(new_database_path) - - -def new_db_version_ok(new_database_path): - # Let's check if we converted all/some entries before - with contextlib.closing(sqlite3.connect(new_database_path)) as connection, connection: - cursor = connection.cursor() - cursor.execute('SELECT value FROM MiscData WHERE name == "db_version"') - version = int(cursor.fetchone()[0]) - return False if version != CURRENT_DB_VERSION else True - - -def already_upgraded(new_database_path): - connection = sqlite3.connect(new_database_path) - # Check if already upgraded - cursor = connection.cursor() - cursor.execute(f'SELECT value FROM MiscData WHERE name == "{CONVERSION_FROM_72}"') - result = cursor.fetchone() - if result: - state = result[0] - if state == CONVERSION_FINISHED: - return True - connection.close() - return False - - -def should_upgrade(old_database_path, new_database_path, logger=None): - """ - Decide if we can migrate data from old DB to Pony - :return: False if something goes wrong, or we don't need/cannot migrate data - """ - if not old_database_path.exists(): - # no old DB to upgrade - return False - - try: - if not old_db_version_ok(old_database_path): - return False - except: - logger.error("Can't open the old tribler.sdb file") - return False - - if new_database_path.exists(): - try: - if not new_db_version_ok(new_database_path): - return False - if already_upgraded(new_database_path): - return False - except: - logger.error("Error while trying to open Pony DB file %s", new_database_path) - return False - - return True diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/conftest.py b/src/tribler-core/tribler_core/components/upgrade/implementation/tests/conftest.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_config_upgrade_to_74.py b/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_config_upgrade_to_74.py deleted file mode 100644 index 894e3f3ba8d..00000000000 --- a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_config_upgrade_to_74.py +++ /dev/null @@ -1,109 +0,0 @@ -import os -import shutil -from pathlib import Path - -from configobj import ParseError as ConfigObjParseError - -import pytest - -from tribler_common.simpledefs import STATEDIR_CHECKPOINT_DIR -from tribler_core.components.upgrade.implementation.config_converter import convert_state_file_to_conf_74 - -from tribler_core.tests.tools.common import TESTS_DATA_DIR -from tribler_core.components.upgrade.implementation import config_converter - -CONFIG_PATH = TESTS_DATA_DIR / "config_files" - - -# pylint: disable=import-outside-toplevel, unused-argument - -@pytest.fixture(name='refactoring_tool') -def fixture_refactoring_tool(): - from lib2to3.refactor import RefactoringTool, get_fixers_from_package - return RefactoringTool(fixer_names=get_fixers_from_package('lib2to3.fixes')) - - -def test_convert_state_file_to_conf_74(tmpdir, refactoring_tool): - """ - Tests conversion of the pstate files (pre-7.4.0) files to .conf files. Tests for two different pstate files, - one corrupted with incorrect metainfo data, and the other one with correct working metadata. - """ - os.makedirs(Path(tmpdir) / STATEDIR_CHECKPOINT_DIR) - - # Copy a good working Ubuntu pstate file - src_path = Path(CONFIG_PATH, "194257a7bf4eaea978f4b5b7fbd3b4efcdd99e43.state") - dest_path = Path(tmpdir) / STATEDIR_CHECKPOINT_DIR / "ubuntu_ok.state" - - shutil.copyfile(src_path, dest_path) - convert_state_file_to_conf_74(dest_path, refactoring_tool) - - converted_file_path = Path(tmpdir) / STATEDIR_CHECKPOINT_DIR / "ubuntu_ok.conf" - assert os.path.exists(converted_file_path) - assert not os.path.exists(dest_path) - - os.remove(converted_file_path) - - # Copy Ubuntu pstate file with corrupted metainfo data - src_path = CONFIG_PATH / "194257a7bf4eaea978f4b5b7fbd3b4efcdd99e43_corrupted.state" - dest_path = Path(tmpdir) / STATEDIR_CHECKPOINT_DIR / "ubuntu_corrupted.state" - - shutil.copyfile(src_path, dest_path) - convert_state_file_to_conf_74(dest_path, refactoring_tool) - - converted_file_path = Path(tmpdir) / STATEDIR_CHECKPOINT_DIR / "ubuntu_corrupted.conf" - assert not os.path.exists(converted_file_path) - assert not os.path.exists(dest_path) - - -def test_no_refactoring_tool(tmpdir): - state_conf = "missed_state.conf" - - config_path = Path(tmpdir, state_conf) - shutil.copy(Path(CONFIG_PATH, state_conf), config_path) - - assert convert_state_file_to_conf_74(config_path, None) - - -def test_missed_state(tmpdir, refactoring_tool): - state_conf = "missed_state.conf" - - config_path = Path(tmpdir, state_conf) - shutil.copy(Path(CONFIG_PATH, state_conf), config_path) - - assert convert_state_file_to_conf_74(config_path, refactoring_tool) - - -@pytest.fixture(name='faulty_config_parser') -async def fixture_faulty_config_parser(): - def load_config_with_parse_error(filename): - raise ConfigObjParseError(f"Error parsing config file {filename}") - - original_load_config = config_converter.load_config - config_converter.load_config = load_config_with_parse_error - yield config_converter - config_converter.load_config = original_load_config - - -def test_convert_state_file_to_conf74_with_parse_error(tmpdir, faulty_config_parser, refactoring_tool): - """ - Tests conversion of the state files (pre-7.4.0) files to .conf files (7.4) when there is parsing error. - ParseError happens for some users for some unknown reason. We simply remove the files that we cannot - parse. This test tests that such a corrupted file is actual deleted. - """ - state_dir = Path(tmpdir) - os.makedirs(state_dir / STATEDIR_CHECKPOINT_DIR) - - # Copy Ubuntu pstate file with corrupted metainfo data - src_path = CONFIG_PATH / "194257a7bf4eaea978f4b5b7fbd3b4efcdd99e43.state" - dest_path = state_dir / STATEDIR_CHECKPOINT_DIR / "ubuntu_corrupted.state" - shutil.copyfile(src_path, dest_path) - - # Try converting the state file to 7.4 conf format - convert_state_file_to_conf_74(dest_path, refactoring_tool) - - # Path where the file should be saved after conversion - converted_file_path = state_dir / STATEDIR_CHECKPOINT_DIR / "ubuntu_corrupted.conf" - - # We expect ParseError and the file to be deleted. - assert not os.path.exists(converted_file_path) - assert not os.path.exists(dest_path) diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_config_upgrade_to_75.py b/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_config_upgrade_to_75.py deleted file mode 100644 index db635872fa3..00000000000 --- a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_config_upgrade_to_75.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import shutil -from pathlib import Path - -from tribler_common.simpledefs import STATEDIR_CHECKPOINT_DIR -from tribler_core.components.upgrade.implementation.config_converter import convert_config_to_tribler75 - -from tribler_core.tests.tools.common import TESTS_DATA_DIR - -CONFIG_PATH = TESTS_DATA_DIR / "config_files" - - -def test_convert_state_file_to_conf75_with_parse_error(tmpdir): - """ - Tests conversion of the conf files (7.4.0) files to .conf files (7.5) when there is parsing error. - ParseError happens for some users for some unknown reason. We simply remove the files that we cannot - parse. This test tests that such a corrupted file is actual deleted. - """ - state_dir = Path(tmpdir) - os.makedirs(state_dir / STATEDIR_CHECKPOINT_DIR) - - # Copy Ubuntu conf file with corrupted metainfo data - src_path = CONFIG_PATH / "corrupt_download_config.conf" - dest_path = state_dir / STATEDIR_CHECKPOINT_DIR / "ubuntu_corrupted.conf" - shutil.copyfile(src_path, dest_path) - - # Try converting the conf file to 7.5 conf format - convert_config_to_tribler75(Path(tmpdir)) - - # Path where the file should be saved after conversion - converted_file_path = state_dir / STATEDIR_CHECKPOINT_DIR / "ubuntu_corrupted.conf" - - # We expect ParseError and the file to be deleted. - assert not os.path.exists(converted_file_path) - assert not os.path.exists(dest_path) diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_legacy_to_pony.py b/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_legacy_to_pony.py deleted file mode 100644 index f239a1337c4..00000000000 --- a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_legacy_to_pony.py +++ /dev/null @@ -1,189 +0,0 @@ -import contextlib -import shutil -import sqlite3 -from pathlib import Path -from unittest.mock import Mock - -from ipv8.keyvault.crypto import default_eccrypto - -from pony.orm import db_session - -import pytest - -from tribler_core.components.metadata_store.db.orm_bindings.channel_node import COMMITTED, LEGACY_ENTRY -from tribler_core.components.metadata_store.db.store import MetadataStore -from tribler_core.components.upgrade.implementation.legacy_to_pony import CONVERSION_FINISHED, \ - CONVERSION_FROM_72, CONVERSION_FROM_72_CHANNELS, CONVERSION_FROM_72_DISCOVERED, CONVERSION_FROM_72_PERSONAL, \ - CONVERSION_STARTED, \ - already_upgraded, new_db_version_ok, old_db_version_ok -from tribler_core.tests.tools.common import TESTS_DATA_DIR -from tribler_core.components.upgrade.implementation import legacy_to_pony -from tribler_core.components.upgrade.implementation.upgrade import ( - cleanup_pony_experimental_db, - should_upgrade, -) - -OLD_DB_SAMPLE = TESTS_DATA_DIR / 'upgrade_databases/tribler_v29.sdb' - - -def test_get_personal_channel_title(dispersy_to_pony_migrator): - assert dispersy_to_pony_migrator.personal_channel_title - - -def test_get_old_torrents_count(dispersy_to_pony_migrator): - assert dispersy_to_pony_migrator.get_old_torrents_count() == 19 - - -def test_get_personal_torrents_count(dispersy_to_pony_migrator): - assert dispersy_to_pony_migrator.get_personal_channel_torrents_count() == 2 - - -@pytest.mark.asyncio -async def test_convert_personal_channel(dispersy_to_pony_migrator, metadata_store): - async def check_channel(): - await dispersy_to_pony_migrator.convert_personal_channel() - with db_session: - my_channel = metadata_store.ChannelMetadata.get_my_channels().first() - - assert len(my_channel.contents_list) == 2 - assert my_channel.num_entries == 2 - for t in my_channel.contents_list: - assert t.has_valid_signature() - assert my_channel.has_valid_signature() - assert dispersy_to_pony_migrator.personal_channel_title[:200] == my_channel.title - - await check_channel() - - # Now check the case where previous conversion of the personal channel had failed - with db_session: - metadata_store.MiscData.get_for_update(name=CONVERSION_FROM_72_PERSONAL).value = CONVERSION_STARTED - await check_channel() - - -@pytest.mark.asyncio -@db_session -async def test_convert_legacy_channels(dispersy_to_pony_migrator, metadata_store): - async def check_conversion(): - await dispersy_to_pony_migrator.convert_discovered_torrents() - dispersy_to_pony_migrator.convert_discovered_channels() - chans = metadata_store.get_entries(cls=metadata_store.ChannelMetadata) - - assert len(chans) == 2 - for c in chans: - assert dispersy_to_pony_migrator.personal_channel_title[:200] != c.title[:200] - assert c.status == LEGACY_ENTRY - assert c.contents_list - for t in c.contents_list: - assert t.status == COMMITTED - await check_conversion() - - # Now check the case where the previous conversion failed at channels conversion - metadata_store.MiscData.get_for_update(name=CONVERSION_FROM_72_CHANNELS).value = CONVERSION_STARTED - await check_conversion() - - # Now check the case where the previous conversion stopped at torrents conversion - metadata_store.MiscData.get_for_update(name=CONVERSION_FROM_72_CHANNELS).delete() - metadata_store.MiscData.get_for_update(name=CONVERSION_FROM_72_DISCOVERED).value = CONVERSION_STARTED - for d in metadata_store.TorrentMetadata.select()[:10][:10]: - d.delete() - await check_conversion() - - -@db_session -def test_update_trackers(dispersy_to_pony_migrator, metadata_store): - tr = metadata_store.TrackerState(url="http://ipv6.torrent.ubuntu.com:6969/announce") - dispersy_to_pony_migrator.update_trackers_info() - assert tr.failures == 2 - assert tr.alive - assert tr.last_check == 1548776649 - - -def test_old_db_version_check(tmpdir): - # Correct old database - assert old_db_version_ok(OLD_DB_SAMPLE) - - # Wrong old database version - old_db = tmpdir / 'old.db' - shutil.copyfile(OLD_DB_SAMPLE, old_db) - with contextlib.closing(sqlite3.connect(old_db)) as connection, connection: - cursor = connection.cursor() - cursor.execute("UPDATE MyInfo SET value = 28 WHERE entry == 'version'") - assert not old_db_version_ok(old_db) - - -def test_cleanup_pony_experimental_db(tmpdir, metadata_store): - # Assert True is returned for a garbled db and nothing is done with it - garbled_db = tmpdir / 'garbled.db' - with open(garbled_db, 'w') as f: - f.write("123") - - with pytest.raises(sqlite3.DatabaseError): - cleanup_pony_experimental_db(garbled_db) - assert garbled_db.exists() - - # Create a Pony database of older experimental version - pony_db = Path(tmpdir) / 'test.db' - pony_db_bak = Path(tmpdir) / 'pony2.db' - metadata_store.shutdown() - shutil.copyfile(pony_db, pony_db_bak) - - with contextlib.closing(sqlite3.connect(pony_db)) as connection, connection: - cursor = connection.cursor() - cursor.execute("DROP TABLE MiscData") - - # Assert older experimental version is deleted - cleanup_pony_experimental_db(pony_db) - assert not pony_db.exists() - - # Assert recent database version is left untouched - cleanup_pony_experimental_db(pony_db_bak) - assert pony_db_bak.exists() - - -def test_new_db_version_ok(tmpdir, metadata_store): - pony_db_path = Path(tmpdir) / 'test.db' - metadata_store.shutdown() - - # Correct new database - assert new_db_version_ok(pony_db_path) - - # Wrong new database version - with contextlib.closing(sqlite3.connect(pony_db_path)) as connection, connection: - cursor = connection.cursor() - cursor.execute("UPDATE MiscData SET value = 12313512 WHERE name == 'db_version'") - assert not new_db_version_ok(pony_db_path) - - -def test_already_upgraded(tmpdir, metadata_store): - pony_db_path = Path(tmpdir) / 'test.db' - my_key = default_eccrypto.generate_key("curve25519") - metadata_store.shutdown() - - assert not already_upgraded(pony_db_path) - - mds = MetadataStore(pony_db_path, tmpdir, my_key) - with db_session: - mds.MiscData(name=CONVERSION_FROM_72, value=CONVERSION_FINISHED) - mds.shutdown() - - assert already_upgraded(pony_db_path) - - -def test_should_upgrade(tmpdir): - pony_db = tmpdir / 'pony.db' - - # Old DB does not exist - assert not should_upgrade(Path(tmpdir) / 'nonexistent.db', None) - - # Old DB is not OK - legacy_to_pony.old_db_version_ok = lambda _: False - assert not should_upgrade(OLD_DB_SAMPLE, None) - - # Pony DB does not exist - legacy_to_pony.old_db_version_ok = lambda _: True - assert should_upgrade(OLD_DB_SAMPLE, pony_db) - - # Bad Pony DB - with open(pony_db, 'w') as f: - f.write("") - assert not should_upgrade(OLD_DB_SAMPLE, pony_db, logger=Mock()) diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_upgrader.py b/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_upgrader.py index 45c0f3a47d1..1c424bba363 100644 --- a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_upgrader.py +++ b/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_upgrader.py @@ -4,17 +4,22 @@ from pathlib import Path from unittest.mock import Mock -import pytest +from ipv8.keyvault.private.libnaclkey import LibNaCLSK + from pony.orm import db_session, select -from ipv8.keyvault.private.libnaclkey import LibNaCLSK +import pytest + from tribler_common.simpledefs import NTFY + from tribler_core.components.bandwidth_accounting.db.database import BandwidthDatabase from tribler_core.components.metadata_store.db.orm_bindings.channel_metadata import CHANNEL_DIR_NAME_LENGTH from tribler_core.components.metadata_store.db.store import CURRENT_DB_VERSION, MetadataStore from tribler_core.components.upgrade.implementation.db8_to_db10 import calc_progress -from tribler_core.components.upgrade.implementation.upgrade import TriblerUpgrader, \ - cleanup_noncompliant_channel_torrents +from tribler_core.components.upgrade.implementation.upgrade import ( + TriblerUpgrader, + cleanup_noncompliant_channel_torrents, +) from tribler_core.notifier import Notifier from tribler_core.tests.tools.common import TESTS_DATA_DIR from tribler_core.utilities.configparser import CallbackConfigParser @@ -62,63 +67,12 @@ def on_upgrade_tick(status_text): await test_future -@pytest.mark.asyncio -async def test_upgrade_72_to_pony(upgrader, channels_dir, state_dir, trustchain_keypair): - old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'tribler_v29.sdb' - old_database_path = state_dir / 'sqlite' / 'tribler.sdb' - new_database_path = state_dir / 'sqlite' / 'metadata.db' - shutil.copyfile(old_db_sample, old_database_path) - - await upgrader.run() - mds = MetadataStore(new_database_path, channels_dir, trustchain_keypair, db_version=6) - with db_session: - assert mds.TorrentMetadata.select().count() == 24 - mds.shutdown() - - -def test_upgrade_pony_db_6to7(upgrader, channels_dir, state_dir, trustchain_keypair): - """ - Test that channels and torrents with forbidden words are cleaned up during upgrade from Pony db ver 6 to 7. - Also, check that the DB version is upgraded. - :return: - """ - old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'pony_v6.db' - old_database_path = state_dir / 'sqlite' / 'metadata.db' - shutil.copyfile(old_db_sample, old_database_path) - - upgrader.upgrade_pony_db_6to7() - mds = MetadataStore(old_database_path, channels_dir, trustchain_keypair, check_tables=False, db_version=7) - with db_session: - assert mds.TorrentMetadata.select().count() == 23 - assert mds.ChannelMetadata.select().count() == 2 - assert int(mds.MiscData.get(name="db_version").value) == 7 - mds.shutdown() - - -def test_upgrade_pony_db_7to8(upgrader, channels_dir, state_dir, trustchain_keypair): - """ - Test that proper additional index is created. - Also, check that the DB version is upgraded. - """ - old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'pony_v7.db' - old_database_path = state_dir / 'sqlite' / 'metadata.db' - shutil.copyfile(old_db_sample, old_database_path) - - upgrader.upgrade_pony_db_7to8() - mds = MetadataStore(old_database_path, channels_dir, trustchain_keypair, check_tables=False, db_version=8) - with db_session: - assert int(mds.MiscData.get(name="db_version").value) == 8 - assert mds.Vsids[0].exp_period == 24.0 * 60 * 60 * 3 - assert list(mds._db.execute('PRAGMA index_info("idx_channelnode__metadata_type")')) - mds.shutdown() - - @pytest.mark.asyncio async def test_upgrade_pony_db_complete(upgrader, channels_dir, state_dir, trustchain_keypair): """ Test complete update sequence for Pony DB (e.g. 6->7->8) """ - old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'pony_v6.db' + old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'pony_v8.db' old_database_path = state_dir / 'sqlite' / 'metadata.db' shutil.copyfile(old_db_sample, old_database_path) @@ -158,24 +112,6 @@ async def test_upgrade_pony_db_complete(upgrader, channels_dir, state_dir, trust assert upgrader.trigger_exists(db, 'torrentstate_au') mds.shutdown() - -@pytest.mark.asyncio -async def test_skip_upgrade_72_to_pony(upgrader, channels_dir, state_dir, trustchain_keypair): - old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'tribler_v29.sdb' - old_database_path = state_dir / 'sqlite' / 'tribler.sdb' - new_database_path = state_dir / 'sqlite' / 'metadata.db' - - shutil.copyfile(old_db_sample, old_database_path) - - upgrader.skip() - await upgrader.run() - mds = MetadataStore(new_database_path, channels_dir, trustchain_keypair, db_version=6) - with db_session: - assert mds.TorrentMetadata.select().count() == 0 - assert mds.ChannelMetadata.select().count() == 0 - mds.shutdown() - - def test_delete_noncompliant_state(tmpdir): state_dir = TESTS_DATA_DIR / 'noncompliant_state_dir' shutil.copytree(str(state_dir), str(tmpdir / "test")) @@ -199,12 +135,10 @@ def test_delete_noncompliant_state(tmpdir): @pytest.mark.asyncio async def test_upgrade_pony_8to10(upgrader, channels_dir, state_dir, trustchain_keypair): - old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'pony_v6.db' + old_db_sample = TESTS_DATA_DIR / 'upgrade_databases' / 'pony_v8.db' database_path = state_dir / 'sqlite' / 'metadata.db' shutil.copyfile(old_db_sample, database_path) - upgrader.upgrade_pony_db_6to7() - upgrader.upgrade_pony_db_7to8() await upgrader.upgrade_pony_db_8to10() mds = MetadataStore(database_path, channels_dir, trustchain_keypair, check_tables=False, db_version=10) with db_session: diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/upgrade.py b/src/tribler-core/tribler_core/components/upgrade/implementation/upgrade.py index fca8a8fa25d..e4688abfdc2 100644 --- a/src/tribler-core/tribler_core/components/upgrade/implementation/upgrade.py +++ b/src/tribler-core/tribler_core/components/upgrade/implementation/upgrade.py @@ -5,23 +5,18 @@ from pony.orm import db_session, delete -from tribler_common.simpledefs import NTFY, STATEDIR_DB_DIR, STATEDIR_CHANNELS_DIR +from tribler_common.simpledefs import NTFY, STATEDIR_CHANNELS_DIR, STATEDIR_DB_DIR from tribler_core.components.bandwidth_accounting.db.database import BandwidthDatabase -from tribler_core.components.metadata_store.category_filter.l2_filter import is_forbidden from tribler_core.components.metadata_store.db.orm_bindings.channel_metadata import CHANNEL_DIR_NAME_LENGTH -from tribler_core.components.metadata_store.db.serialization import CHANNEL_TORRENT from tribler_core.components.metadata_store.db.store import ( MetadataStore, sql_create_partial_index_channelnode_metadata_type, sql_create_partial_index_channelnode_subscribed, sql_create_partial_index_torrentstate_last_check, ) -from tribler_core.components.upgrade.implementation.config_converter import convert_config_to_tribler74, \ - convert_config_to_tribler75, convert_config_to_tribler76 +from tribler_core.components.upgrade.implementation.config_converter import convert_config_to_tribler76 from tribler_core.components.upgrade.implementation.db8_to_db10 import PonyToPonyMigration, get_db_version -from tribler_core.components.upgrade.implementation.legacy_to_pony import DispersyToPonyMigration, \ - cleanup_pony_experimental_db, should_upgrade from tribler_core.notifier import Notifier from tribler_core.utilities.configparser import CallbackConfigParser @@ -92,18 +87,9 @@ def skip(self): async def run(self): """ Run the upgrader if it is enabled in the config. - - Note that by default, upgrading is enabled in the config. It is then disabled - after upgrading to Tribler 7. """ - - await self.upgrade_72_to_pony() - self.upgrade_pony_db_6to7() - self.upgrade_pony_db_7to8() await self.upgrade_pony_db_8to10() self.upgrade_pony_db_10to11() - convert_config_to_tribler74(self.state_dir) - convert_config_to_tribler75(self.state_dir) convert_config_to_tribler76(self.state_dir) self.upgrade_bw_accounting_db_8to9() self.upgrade_pony_db_11to12() @@ -309,109 +295,9 @@ async def upgrade_pony_db_8to10(self): self.notify_done() - def upgrade_pony_db_7to8(self): - """ - Upgrade GigaChannel DB from version 7 (7.4.x) to version 8 (7.5.x). - Migration should be relatively fast, so we do it in the foreground. - """ - # We have to create the Metadata Store object because Session-managed Store has not been started yet - database_path = self.state_dir / STATEDIR_DB_DIR / 'metadata.db' - if not database_path.exists(): - return - mds = MetadataStore(database_path, self.channels_dir, self.trustchain_keypair, - disable_sync=True, check_tables=False, db_version=7) - self.do_upgrade_pony_db_7to8(mds) - mds.shutdown() - - def do_upgrade_pony_db_7to8(self, mds): - with db_session: - db_version = mds.MiscData.get(name="db_version") - if int(db_version.value) != 7: - return - # Just in case, we skip index creation if it is somehow already there - if not list(mds._db.execute('PRAGMA index_info("idx_channelnode__metadata_type")')): - sql = 'CREATE INDEX "idx_channelnode__metadata_type" ON "ChannelNode" ("metadata_type")' - mds._db.execute(sql) - mds.Vsids[0].exp_period = 24.0 * 60 * 60 * 3 - db_version = mds.MiscData.get(name="db_version") - db_version.value = str(8) - return - - def upgrade_pony_db_6to7(self): - """ - Upgrade GigaChannel DB from version 6 (7.3.0) to version 7 (7.3.1). - Migration should be relatively fast, so we do it in the foreground, without notifying the user - and breaking it in smaller chunks as we do with 72_to_pony. - """ - # We have to create the Metadata Store object because Session-managed Store has not been started yet - database_path = self.state_dir / STATEDIR_DB_DIR / 'metadata.db' - if not database_path.exists(): - return - mds = MetadataStore(database_path, self.channels_dir, self.trustchain_keypair, - disable_sync=True, check_tables=False, db_version=6) - self.do_upgrade_pony_db_6to7(mds) - mds.shutdown() - - def do_upgrade_pony_db_6to7(self, mds): - with db_session: - db_version = mds.MiscData.get(name="db_version") - if int(db_version.value) != 6: - return - for c in mds.ChannelMetadata.select_by_sql(f""" - select rowid, title, tags, metadata_type from ChannelNode - where metadata_type = {CHANNEL_TORRENT} - """): - if is_forbidden(c.title+c.tags): - c.contents.delete() - c.delete() - # The channel torrent will be removed by GigaChannel manager during the cruft cleanup - - # The process is broken down into batches to limit memory usage - batch_size = 10000 - with db_session: - total_entries = mds.TorrentMetadata.select().count() - page_num = total_entries // batch_size - while page_num >= 0: - with db_session: - for t in mds.TorrentMetadata.select().page(page_num, pagesize=batch_size): - if is_forbidden(t.title+t.tags): - t.delete() - page_num -= 1 - with db_session: - db_version = mds.MiscData.get(name="db_version") - db_version.value = str(7) - return - def update_status(self, status_text): self.notifier.notify(NTFY.UPGRADER_TICK, status_text) - async def upgrade_72_to_pony(self): - old_database_path = self.state_dir / STATEDIR_DB_DIR / 'tribler.sdb' - new_database_path = self.state_dir / STATEDIR_DB_DIR / 'metadata.db' - if new_database_path.exists(): - cleanup_pony_experimental_db(str(new_database_path)) - cleanup_noncompliant_channel_torrents(self.state_dir) - - self._dtp72 = DispersyToPonyMigration(old_database_path, self.update_status, logger=self._logger) - if not should_upgrade(old_database_path, new_database_path, logger=self._logger): - self._dtp72 = None - return - # This thing is here mostly for the skip upgrade test to work... - self._dtp72.shutting_down = self.skip_upgrade_called - self.notify_starting() - # We have to create the Metadata Store object because Session-managed Store has not been started yet - mds = MetadataStore(new_database_path, self.channels_dir, self.trustchain_keypair, - disable_sync=True, db_version=6) - self._dtp72.initialize(mds) - - try: - await self._dtp72.do_migration() - except Exception as e: - self._logger.error("Error in Upgrader callback chain: %s", e) - finally: - mds.shutdown() - self.notify_done() - def notify_starting(self): """ Broadcast a notification (event) that the upgrader is starting doing work diff --git a/src/tribler-core/tribler_core/conftest.py b/src/tribler-core/tribler_core/conftest.py index 271fcbdd3d4..8f8295c1650 100644 --- a/src/tribler-core/tribler_core/conftest.py +++ b/src/tribler-core/tribler_core/conftest.py @@ -21,7 +21,6 @@ from tribler_core.components.libtorrent.torrentdef import TorrentDef from tribler_core.components.metadata_store.db.store import MetadataStore from tribler_core.components.tag.db.tag_db import TagDatabase -from tribler_core.components.upgrade.implementation.legacy_to_pony import DispersyToPonyMigration from tribler_core.config.tribler_config import TriblerConfig from tribler_core.tests.tools.common import TESTS_DATA_DIR, TESTS_DIR from tribler_core.tests.tools.tracker.udp_tracker import UDPTracker @@ -205,14 +204,6 @@ def tags_db(): db.shutdown() -@pytest.fixture -def dispersy_to_pony_migrator(metadata_store): - dispersy_db_path = TESTS_DATA_DIR / 'upgrade_databases/tribler_v29.sdb' - migrator = DispersyToPonyMigration(dispersy_db_path) - migrator.initialize(metadata_store) - return migrator - - @pytest.fixture def enable_https(tribler_config, free_port): tribler_config.api.put_path_as_relative('https_certfile', TESTS_DIR / 'data' / 'certfile.pem', diff --git a/src/tribler-core/tribler_core/tests/tools/data/upgrade_databases/pony_v6.db b/src/tribler-core/tribler_core/tests/tools/data/upgrade_databases/pony_v8.db similarity index 79% rename from src/tribler-core/tribler_core/tests/tools/data/upgrade_databases/pony_v6.db rename to src/tribler-core/tribler_core/tests/tools/data/upgrade_databases/pony_v8.db index 50dc01086ff8517b1161dc71062ca1a69de50d8e..9f859d00d6b64e8573cec2902342de3c0c9643a2 100644 GIT binary patch delta 9008 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 delta 11506 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 diff --git a/src/tribler-core/tribler_core/tests/tools/data/upgrade_databases/tribler_v29.sdb b/src/tribler-core/tribler_core/tests/tools/data/upgrade_databases/tribler_v29.sdb deleted file mode 100644 index 429caabd0ab1cd9c0fc036a873c3ef15f1f1d347..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 598016 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 From 43ef06b576725fa46963f8f255e5954bb80f5a79 Mon Sep 17 00:00:00 2001 From: "V.G. Bulavintsev" Date: Tue, 16 Nov 2021 17:09:29 +0100 Subject: [PATCH 2/2] Remove support for dir copying for versions prior to 7.5 --- .../tribler_common/version_manager.py | 13 +--- .../tests/test_version_manager.py | 71 +------------------ 2 files changed, 2 insertions(+), 82 deletions(-) diff --git a/src/tribler-common/tribler_common/version_manager.py b/src/tribler-common/tribler_common/version_manager.py index f5e21016d8e..e251c6a3785 100644 --- a/src/tribler-common/tribler_common/version_manager.py +++ b/src/tribler-common/tribler_common/version_manager.py @@ -57,7 +57,7 @@ In some sense, the system works exactly as GIT does: it "branches" from the last-non conflicting version dir and "stashes" the state dirs with conflicting names by renaming them. -Note that due to failures in design pre-7.4 series and 7.4.x series get special treatment. +Versions prior to 7.5 are not supported. """ @@ -100,12 +100,6 @@ def __repr__(self): return f'<{self.__class__.__name__}{{{self.version_str}}}>' def get_directory(self): - if self.major_minor < (7, 4): - # This should only happen for old "7.0.0-GIT" case - return self.root_state_dir - if self.major_minor == (7, 4): - # 7.4.x are treated specially - return self.root_state_dir / (".".join(str(part) for part in LooseVersion(self.version_str).version[:3])) return self.root_state_dir / ('%d.%d' % self.major_minor) def state_exists(self): @@ -207,11 +201,6 @@ def __init__(self, root_state_dir: Path, code_version_id: Optional[str] = None): self.versions = versions = OrderedDict() if self.file_path.exists(): self.load(self.file_path) - elif (root_state_dir / "triblerd.conf").exists(): - # Pre-7.4 versions of Tribler don't have history file - # and can by detected by presence of the triblerd.conf file in the root directory - version = TriblerVersion(root_state_dir, "7.3", 0.0) - self.add_version(version) versions_by_time = [] last_run_version = None diff --git a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_version_manager.py b/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_version_manager.py index 1001009a1e9..4d339b01d54 100644 --- a/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_version_manager.py +++ b/src/tribler-core/tribler_core/components/upgrade/implementation/tests/test_version_manager.py @@ -32,12 +32,6 @@ def version_to_dirname(version_str): assert version_to_dirname("7.5") == Path("/ROOT/7.5") assert version_to_dirname("7.5.0") == Path("/ROOT/7.5") - # These are special cases of 7.4.x series that used patch version naming - assert version_to_dirname("7.4.4") == Path("/ROOT/7.4.4") - - # Versions earlier then 7.4 should return root directory - assert version_to_dirname("7.3.0") == Path("/ROOT") - def test_read_write_version_history(tmpdir): root_path = Path(tmpdir) @@ -165,34 +159,6 @@ def test_fork_state_directory(tmpdir_factory): assert history2.last_run_version == history2.code_version assert history2.last_run_version.version_str == code_version_id - # Scenario 3: upgrade from 7.3 (unversioned dir) - # dir should be forked and version_history should be created - tmpdir = tmpdir_factory.mktemp("scenario3") - root_state_dir = Path(tmpdir) - (root_state_dir / "triblerd.conf").write_text("foo") # 7.3 presence marker - code_version_id = "120.3.2" - - history = VersionHistory(root_state_dir, code_version_id) - assert history.last_run_version is not None - assert history.last_run_version.directory == root_state_dir - assert history.code_version != history.last_run_version - assert history.code_version.directory != root_state_dir - assert history.code_version.should_be_copied - assert history.code_version.can_be_copied_from is not None - assert history.code_version.can_be_copied_from.version_str == "7.3" - assert not history.code_version.should_recreate_directory - assert not history.code_version.directory.exists() - - forked_from = history.fork_state_directory_if_necessary() - assert history.code_version.directory.exists() - assert forked_from is not None and forked_from.version_str == "7.3" - history_saved = history.save_if_necessary() - assert history_saved - - history2 = VersionHistory(root_state_dir, code_version_id) - assert history2.last_run_version == history2.code_version - assert history2.last_run_version.version_str == code_version_id - # Scenario 4: the user tried to upgrade to some tribler version, but failed. Now he tries again with # higher patch version of the same major/minor version. # The most recently used dir with major/minor version lower than the code version should be forked, @@ -240,37 +206,6 @@ def test_fork_state_directory(tmpdir_factory): assert history2.last_run_version == history2.code_version assert history2.last_run_version.version_str == code_version_id - # Scenario 5: normal upgrade scenario, but from 7.4.x version (dir includes patch number) - tmpdir = tmpdir_factory.mktemp("scenario5") - root_state_dir = Path(tmpdir) - json_dict = {"last_version": "7.4.4", "history": dict()} - json_dict["history"]["2"] = "7.4.4" - state_dir = root_state_dir / "7.4.4" - state_dir.mkdir() - (root_state_dir / VERSION_HISTORY_FILENAME).write_text(json.dumps(json_dict)) - - code_version_id = "7.5.1" - - history = VersionHistory(root_state_dir, code_version_id) - assert history.last_run_version is not None - assert history.last_run_version.directory == state_dir - assert history.code_version != history.last_run_version - assert history.code_version.directory != root_state_dir - assert history.code_version.should_be_copied - assert history.code_version.can_be_copied_from is not None - assert history.code_version.can_be_copied_from.version_str == "7.4.4" - assert not history.code_version.directory.exists() - assert not history.code_version.should_recreate_directory - - forked_from = history.fork_state_directory_if_necessary() - assert history.code_version.directory.exists() - assert forked_from is not None and forked_from.version_str == "7.4.4" - history_saved = history.save_if_necessary() - assert history_saved - - history2 = VersionHistory(root_state_dir, code_version_id) - assert history2.last_run_version == history2.code_version - assert history2.last_run_version.version_str == code_version_id def test_copy_state_directory(tmpdir): @@ -405,10 +340,6 @@ def test_installed_versions_and_removal(tmpdir_factory): # pylint: disable=too-many-statements def test_coverage(tmpdir): root_state_dir = Path(tmpdir) - v1 = TriblerVersion(root_state_dir, "7.3.1a") - assert repr(v1) == '' - with pytest.raises(VersionError, match='Cannot rename root directory'): - v1.rename_directory("foo") v2 = TriblerVersion(root_state_dir, "7.8.1") assert v2.directory == root_state_dir / "7.8" @@ -459,7 +390,7 @@ def test_coverage(tmpdir): history = VersionHistory(root_state_dir) assert history.code_version.version_str == tribler_core.version.version_id - assert repr(history) == "" + assert repr(history) == "" dirs = history.get_disposable_state_directories() names = [d.name for d in dirs]