From 937c6c3e8114b14493962f4ca267faf2dace2ac6 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 7 May 2020 05:18:37 +0000 Subject: [PATCH] New service file format --- openlp/core/common/__init__.py | 15 +++ openlp/core/display/window.py | 1 + openlp/core/lib/serviceitem.py | 121 +++++++++++++---- openlp/core/ui/servicemanager.py | 122 +++++++++++++----- openlp/plugins/images/lib/db.py | 4 +- openlp/plugins/images/lib/mediaitem.py | 5 +- openlp/plugins/images/lib/upgrade.py | 25 +++- openlp/plugins/media/lib/mediaitem.py | 4 +- openlp/plugins/presentations/lib/mediaitem.py | 42 +++++- .../lib/presentationcontroller.py | 34 ++++- .../presentations/presentationplugin.py | 6 +- .../openlp_core/lib/test_serviceitem.py | 44 ++++--- .../openlp_plugins/images/test_mediaitem.py | 28 +++- .../openlp_plugins/images/test_upgrade.py | 6 +- .../openlp_core/widgets/test_views.py | 4 +- 15 files changed, 364 insertions(+), 97 deletions(-) diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 543386e84..6039639ad 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -302,6 +302,21 @@ def md5_hash(salt=None, data=None): return hash_value +def sha256_file_hash(filename): + """ + Returns the hashed output of sha256 on the file content using Python3 hashlib + + :param filename: Name of the file to hash + :returns: str + """ + log.debug('sha256_hash(filename="{filename}")'.format(filename=filename)) + hash_obj = hashlib.sha256() + with open(filename, 'rb') as f: + for chunk in iter(lambda: f.read(65536), b''): + hash_obj.update(chunk) + return hash_obj.hexdigest() + + def qmd5_hash(salt=None, data=None): """ Returns the hashed output of MD5Sum on salt, data diff --git a/openlp/core/display/window.py b/openlp/core/display/window.py index 0964bc125..1c47930f8 100644 --- a/openlp/core/display/window.py +++ b/openlp/core/display/window.py @@ -309,6 +309,7 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin): imagesr = copy.deepcopy(images) for image in imagesr: image['path'] = image['path'].as_uri() + image['thumbnail'] = image['thumbnail'].as_uri() json_images = json.dumps(imagesr) self.run_javascript('Display.setImageSlides({images});'.format(images=json_images)) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index bfcee18ed..2237e6b6a 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -28,11 +28,12 @@ import os import uuid from copy import deepcopy from pathlib import Path +from shutil import copytree, copy from PyQt5 import QtGui from openlp.core.state import State -from openlp.core.common import ThemeLevel, md5_hash +from openlp.core.common import ThemeLevel, sha256_file_hash from openlp.core.common.applocation import AppLocation from openlp.core.common.enum import ServiceItemType from openlp.core.common.i18n import translate @@ -104,6 +105,8 @@ class ServiceItem(RegistryProperties): self.timed_slide_interval = 0 self.will_auto_start = False self.has_original_files = True + self.sha256_file_hash = None + self.stored_filename = None self._new_item() self.metadata = [] @@ -275,7 +278,7 @@ class ServiceItem(RegistryProperties): self._print_slides.append(slide) return self._print_slides - def add_from_image(self, path, title, background=None, thumbnail=None): + def add_from_image(self, path, title, background=None, thumbnail=None, file_hash=None): """ Add an image slide to the service item. @@ -287,7 +290,9 @@ class ServiceItem(RegistryProperties): if background: self.image_border = background self.service_item_type = ServiceItemType.Image - slide = {'title': title, 'path': path} + if not file_hash: + file_hash = sha256_file_hash(path) + slide = {'title': title, 'path': path, 'file_hash': file_hash} if thumbnail: slide['thumbnail'] = thumbnail self.slides.append(slide) @@ -311,7 +316,7 @@ class ServiceItem(RegistryProperties): self.slides.append({'title': title, 'text': text, 'verse': verse_tag}) self._new_item() - def add_from_command(self, path, file_name, image, display_title=None, notes=None): + def add_from_command(self, path, file_name, image, display_title=None, notes=None, file_hash=None): """ Add a slide from a command. @@ -320,18 +325,24 @@ class ServiceItem(RegistryProperties): :param image: The command of/for the slide. :param display_title: Title to show in gui/webinterface, optional. :param notes: Notes to show in the webinteface, optional. + :param file_hash: Sha256 hash checksum of the file. """ self.service_item_type = ServiceItemType.Command # If the item should have a display title but this frame doesn't have one, we make one up if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title: display_title = translate('OpenLP.ServiceItem', '[slide {frame:d}]').format(frame=len(self.slides) + 1) + if self.uses_file(): + if file_hash: + self.sha256_file_hash = file_hash + else: + file_location = Path(path) / file_name + self.sha256_file_hash = sha256_file_hash(file_location) + self.stored_filename = '{hash}{ext}'.format(hash=self.sha256_file_hash, ext=os.path.splitext(file_name)[1]) # Update image path to match servicemanager location if file was loaded from service if image and not self.has_original_files and self.name == 'presentations': - file_location = os.path.join(path, file_name) - file_location_hash = md5_hash(file_location.encode('utf-8')) - image = os.path.join(AppLocation.get_section_data_path(self.name), 'thumbnails', file_location_hash, - ntpath.basename(image)) # TODO: Pathlib + image = AppLocation.get_section_data_path(self.name) / 'thumbnails' / self.sha256_file_hash / \ + ntpath.basename(image) self.slides.append({'title': file_name, 'image': image, 'path': path, 'display_title': display_title, 'notes': notes, 'thumbnail': image}) # if self.is_capable(ItemCapabilities.HasThumbnails): @@ -342,6 +353,10 @@ class ServiceItem(RegistryProperties): """ This method returns some text which can be saved into the service file to represent this item. """ + if self.sha256_file_hash: + stored_filename = '{hash}{ext}'.format(hash=self.sha256_file_hash, ext=os.path.splitext(self.title)[1]) + else: + stored_filename = None service_header = { 'name': self.name, 'plugin': self.name, @@ -366,7 +381,9 @@ class ServiceItem(RegistryProperties): 'theme_overwritten': self.theme_overwritten, 'will_auto_start': self.will_auto_start, 'processor': self.processor, - 'metadata': self.metadata + 'metadata': self.metadata, + 'sha256_file_hash': self.sha256_file_hash, + 'stored_filename': stored_filename } service_data = [] if self.service_item_type == ServiceItemType.Text: @@ -378,15 +395,18 @@ class ServiceItem(RegistryProperties): elif self.service_item_type == ServiceItemType.Image: if lite_save: for slide in self.slides: - service_data.append({'title': slide['title'], 'path': slide['path']}) + service_data.append({'title': slide['title'], 'path': slide['path'], + 'file_hash': slide['file_hash']}) else: - service_data = [slide['title'] for slide in self.slides] + for slide in self.slides: + image_path = slide['thumbnail'].relative_to(AppLocation().get_data_path()) + service_data.append({'title': slide['title'], 'image': image_path, 'file_hash': slide['file_hash']}) elif self.service_item_type == ServiceItemType.Command: for slide in self.slides: if isinstance(slide['image'], QtGui.QIcon): - image = "clapperboard" + image = 'clapperboard' else: - image = slide['image'] + image = slide['image'].relative_to(AppLocation().get_data_path()) service_data.append({'title': slide['title'], 'image': image, 'path': slide['path'], 'display_title': slide['display_title'], 'notes': slide['notes']}) return {'header': service_header, 'data': service_data} @@ -398,7 +418,7 @@ class ServiceItem(RegistryProperties): self._display_slides = [] self._rendered_slides = [] - def set_from_service(self, service_item, path=None): + def set_from_service(self, service_item, path=None, version=2): """ This method takes a service item from a saved service file (passed from the ServiceManager) and extracts the data actually required. @@ -406,6 +426,7 @@ class ServiceItem(RegistryProperties): :param service_item: The item to extract data from. :param path: Defaults to *None*. This is the service manager path for things which have their files saved with them or None when the saved service is lite and the original file paths need to be preserved. + :param version: Format version of the data. """ log.debug('set_from_service called with path {path}'.format(path=path)) header = service_item['serviceitem']['header'] @@ -433,11 +454,16 @@ class ServiceItem(RegistryProperties): self.processor = header.get('processor', None) self.has_original_files = True self.metadata = header.get('item_meta_data', []) + self.sha256_file_hash = header.get('sha256_file_hash', None) + self.stored_filename = header.get('stored_filename', self.title) if 'background_audio' in header and State().check_preconditions('media'): self.background_audio = [] for file_path in header['background_audio']: # In OpenLP 3.0 we switched to storing Path objects in JSON files - if isinstance(file_path, str): + if version >= 3: + if path: + file_path = path / file_path + else: # Handle service files prior to OpenLP 3.0 # Windows can handle both forward and backward slashes, so we use ntpath to get the basename file_path = path / ntpath.basename(file_path) @@ -453,26 +479,54 @@ class ServiceItem(RegistryProperties): if path: self.has_original_files = False for text_image in service_item['serviceitem']['data']: - file_path = path / text_image - self.add_from_image(file_path, text_image, background) + file_hash = None + thumbnail = None + if version >= 3: + text = text_image['title'] + file_hash = text_image['file_hash'] + file_path = path / '{base}{ext}'.format(base=file_hash, ext=os.path.splitext(text)[1]) + thumbnail = AppLocation.get_data_path() / text_image['image'] + # copy thumbnail for servicemanager path + copy(path / 'thumbnails' / os.path.basename(text_image['image']), + AppLocation.get_section_data_path(self.name) / 'thumbnails') + else: + text = text_image + file_path = path / text + self.add_from_image(file_path, text, background, thumbnail=thumbnail, file_hash=file_hash) else: for text_image in service_item['serviceitem']['data']: - self.add_from_image(text_image['path'], text_image['title'], background) + file_hash = None + text = text_image['title'] + if version >= 3: + file_hash = text_image['file_hash'] + self.add_from_image(text_image['path'], text, background, file_hash=file_hash) elif self.service_item_type == ServiceItemType.Command: for text_image in service_item['serviceitem']['data']: if not self.title: self.title = text_image['title'] - if self.is_capable(ItemCapabilities.IsOptical): + if self.is_capable(ItemCapabilities.IsOptical) or self.is_capable(ItemCapabilities.CanStream): self.has_original_files = False self.add_from_command(text_image['path'], text_image['title'], text_image['image']) elif path: self.has_original_files = False - if text_image['image'] == "clapperboard": + # Copy any bundled thumbnails into the plugin thumbnail folder + if version >= 3 and os.path.exists(path / self.sha256_file_hash) and \ + os.path.isdir(path / self.sha256_file_hash): + try: + copytree(path / self.sha256_file_hash, + AppLocation.get_section_data_path(self.name) / 'thumbnails' / + self.sha256_file_hash) + except FileExistsError: + # Files already exists, just skip + pass + if text_image['image'] == 'clapperboard': text_image['image'] = UiIcons().clapperboard self.add_from_command(path, text_image['title'], text_image['image'], - text_image.get('display_title', ''), text_image.get('notes', '')) + text_image.get('display_title', ''), text_image.get('notes', ''), + file_hash=self.sha256_file_hash) else: - self.add_from_command(Path(text_image['path']), text_image['title'], text_image['image']) + self.add_from_command(Path(text_image['path']), text_image['title'], text_image['image'], + file_hash=self.sha256_file_hash) self._new_item() def get_display_title(self): @@ -548,7 +602,8 @@ class ServiceItem(RegistryProperties): Confirms if the ServiceItem uses a file """ return self.service_item_type == ServiceItemType.Image or \ - (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical)) + (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical) + and not self.is_capable(ItemCapabilities.CanStream)) def is_text(self): """ @@ -608,6 +663,8 @@ class ServiceItem(RegistryProperties): return '' if self.is_image() or self.is_capable(ItemCapabilities.IsOptical): path_from = frame['path'] + elif self.is_command() and not self.has_original_files and self.sha256_file_hash: + path_from = os.path.join(frame['path'], self.stored_filename) else: path_from = os.path.join(frame['path'], frame['title']) if isinstance(path_from, str): @@ -698,8 +755,11 @@ class ServiceItem(RegistryProperties): self.is_valid = False break else: - file_name = os.path.join(slide['path'], slide['title']) - if not os.path.exists(file_name): + if self.has_original_files: + file_name = Path(slide['path']) / slide['title'] + else: + file_name = Path(slide['path']) / self.stored_filename + if not file_name.exists(): self.is_valid = False break if suffixes and not self.is_text(): @@ -707,3 +767,14 @@ class ServiceItem(RegistryProperties): if file_suffix.lower() not in suffixes: self.is_valid = False break + + def get_thumbnail_path(self): + """ + Returns the thumbnail folder. Should only be used for items that support thumbnails. + """ + if self.is_capable(ItemCapabilities.HasThumbnails): + if self.is_command() and self.slides: + return os.path.dirname(self.slides[0]['image']) + elif self.is_image() and self.slides: + return os.path.dirname(self.slides[0]['thumbnail']) + return None diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 24decd539..d16eef846 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -33,8 +33,8 @@ from tempfile import NamedTemporaryFile from PyQt5 import QtCore, QtGui, QtWidgets +from openlp.core.common import ThemeLevel, delete_file, sha256_file_hash from openlp.core.state import State -from openlp.core.common import ThemeLevel, delete_file from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.applocation import AppLocation from openlp.core.common.enum import ServiceItemType @@ -42,10 +42,10 @@ from openlp.core.common.i18n import UiStrings, format_time, translate from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.registry import Registry, RegistryBase -from openlp.core.lib import build_icon +from openlp.core.lib import build_icon, ItemCapabilities from openlp.core.lib.exceptions import ValidationError from openlp.core.lib.plugin import PluginStatus -from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem +from openlp.core.lib.serviceitem import ServiceItem from openlp.core.lib.ui import create_widget_action, critical_error_message_box, find_and_set_in_combo_box from openlp.core.ui.icons import UiIcons from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT @@ -332,6 +332,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi self._service_path = None self.service_has_all_original_files = True self.list_double_clicked = False + self.servicefile_version = None def bootstrap_initialise(self): """ @@ -518,9 +519,15 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi :return: service array """ service = [] + # Regarding openlp-servicefile-version: + # 1: OpenLP 1? Not used. + # 2: OpenLP 2 (default when loading a service file without openlp-servicefile-version) + # 3: The new format introduced in OpenLP 3.0. + # Note that the servicefile-version numbering is not expected to follow the OpenLP version numbering. core = { 'lite-service': self._save_lite, - 'service-theme': self.service_theme + 'service-theme': self.service_theme, + 'openlp-servicefile-version': 3 } service.append({'openlp_core': core}) return service @@ -530,24 +537,60 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi Get a list of files used in the service and files that are missing. :return: A list of files used in the service that exist, and a list of files that don't. - :rtype: (list[Path], list[Path]) + :rtype: (list[Path], list[str]) """ write_list = [] missing_list = [] + # Run through all items for item in self.service_items: + # If the item has files, see if they exists if item['service_item'].uses_file(): for frame in item['service_item'].get_frames(): path_from = item['service_item'].get_frame_path(frame=frame) - if path_from in write_list or path_from in missing_list: + path_from_path = Path(path_from) + if item['service_item'].stored_filename: + sha256_file_name = item['service_item'].stored_filename + else: + sha256_file_name = sha256_file_hash(path_from_path) + os.path.splitext(path_from)[1] + path_from_tuple = (path_from_path, sha256_file_name) + if path_from_tuple in write_list or str(path_from_path) in missing_list: continue if not os.path.exists(path_from): - missing_list.append(Path(path_from)) + missing_list.append(str(path_from_path)) else: - write_list.append(Path(path_from)) + write_list.append(path_from_tuple) + # For items that has thumbnails, add them to the list + if item['service_item'].is_capable(ItemCapabilities.HasThumbnails): + thumbnail_path = item['service_item'].get_thumbnail_path() + thumbnail_path_parent = Path(thumbnail_path).parent + if item['service_item'].is_command(): + # Run through everything in the thumbnail folder and add pictures + for filename in os.listdir(thumbnail_path): + # Skip non-pictures + if os.path.splitext(filename)[1] not in ['.png', '.jpg']: + continue + filename_path = Path(thumbnail_path) / Path(filename) + # Create a thumbnail path in the zip/service file + service_path = filename_path.relative_to(thumbnail_path_parent) + write_list.append((filename_path, service_path)) + elif item['service_item'].is_image(): + # Find all image thumbnails and store them + # All image thumbnails will be put in a folder named 'thumbnails' + for frame in item['service_item'].get_frames(): + if 'thumbnail' in frame: + filename_path = Path(thumbnail_path) / Path(frame['thumbnail']) + # Create a thumbnail path in the zip/service file + service_path = filename_path.relative_to(thumbnail_path_parent) + path_from_tuple = (filename_path, service_path) + if path_from_tuple in write_list: + continue + write_list.append(path_from_tuple) for audio_path in item['service_item'].background_audio: - if audio_path in write_list: + service_path = sha256_file_hash(audio_path) + os.path.splitext(audio_path)[1] + audio_path_tuple = (audio_path, service_path) + if audio_path_tuple in write_list: continue - write_list.append(audio_path) + write_list.append(audio_path_tuple) return write_list, missing_list def save_file(self): @@ -569,7 +612,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi if not self._save_lite: write_list, missing_list = self.get_write_file_list() - if missing_list: self.application.set_normal_cursor() title = translate('OpenLP.ServiceManager', 'Service File(s) Missing') @@ -596,8 +638,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi service_content = json.dumps(service, cls=OpenLPJSONEncoder) service_content_size = len(bytes(service_content, encoding='utf-8')) total_size = service_content_size - for file_item in write_list: - total_size += file_item.stat().st_size + for local_file_item, zip_file_item in write_list: + total_size += local_file_item.stat().st_size self.log_debug('ServiceManager.save_file - ZIP contents size is %i bytes' % total_size) self.main_window.display_progress_bar(total_size) try: @@ -607,9 +649,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi zip_file.writestr('service_data.osj', service_content) self.main_window.increment_progress_bar(service_content_size) # Finally add all the listed media files. - for write_path in write_list: - zip_file.write(write_path, write_path) - self.main_window.increment_progress_bar(write_path.stat().st_size) + for local_file_item, zip_file_item in write_list: + zip_file.write(str(local_file_item), str(zip_file_item)) + self.main_window.increment_progress_bar(local_file_item.stat().st_size) with suppress(FileNotFoundError): file_path.unlink() os.link(temp_file.name, file_path) @@ -699,11 +741,30 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi service_data = None self.application.set_busy_cursor() try: - with zipfile.ZipFile(file_path) as zip_file: + # TODO: figure out a way to use the presentation thumbnails from the service file + with zipfile.ZipFile(str(file_path)) as zip_file: compressed_size = 0 for zip_info in zip_file.infolist(): compressed_size += zip_info.compress_size self.main_window.display_progress_bar(compressed_size) + # First find the osj-file to find out how to handle the file + for zip_info in zip_file.infolist(): + # The json file has been called 'service_data.osj' since OpenLP 3.0 + if zip_info.filename == 'service_data.osj' or zip_info.filename.endswith('osj'): + with zip_file.open(zip_info, 'r') as json_file: + service_data = json_file.read() + break + if service_data: + items = json.loads(service_data, cls=OpenLPJSONDecoder) + else: + raise ValidationError(msg='No service data found') + # Extract the service file version + for item in items: + if 'openlp_core' in item: + item = item['openlp_core'] + self.servicefile_version = item.get('openlp-servicefile-version', 2) + break + self.log_debug('Service format version: %{ver}'.format(ver=self.servicefile_version)) for zip_info in zip_file.infolist(): self.log_debug('Extract file: {name}'.format(name=zip_info.filename)) # The json file has been called 'service_data.osj' since OpenLP 3.0 @@ -711,19 +772,19 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi with zip_file.open(zip_info, 'r') as json_file: service_data = json_file.read() else: - zip_info.filename = os.path.basename(zip_info.filename) - zip_file.extract(zip_info, self.service_path) + # Service files from earlier versions than 3 expects that all files are extracted + # into the root of the service folder. + if self.servicefile_version and self.servicefile_version < 3: + zip_info.filename = os.path.basename(zip_info.filename.replace('/', os.path.sep)) + zip_file.extract(zip_info, str(self.service_path)) self.main_window.increment_progress_bar(zip_info.compress_size) - if service_data: - items = json.loads(service_data, cls=OpenLPJSONDecoder) + # Handle the content self.new_file() self.process_service_items(items) self.set_file_name(file_path) self.main_window.add_recent_file(file_path) self.set_modified(False) self.settings.setValue('servicemanager/last file', file_path) - else: - raise ValidationError(msg='No service data found') except (NameError, OSError, ValidationError, zipfile.BadZipFile): self.application.set_normal_cursor() self.log_exception('Problem loading service file {name}'.format(name=file_path)) @@ -755,9 +816,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi self.service_theme = theme else: if self._save_lite: - service_item.set_from_service(item) + service_item.set_from_service(item, version=self.servicefile_version) else: - service_item.set_from_service(item, self.service_path) + service_item.set_from_service(item, self.service_path, self.servicefile_version) service_item.validate_item(self.suffixes) if service_item.is_capable(ItemCapabilities.OnLoadUpdate): new_item = Registry().get(service_item.name).service_load(service_item) @@ -1277,11 +1338,12 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi """ Empties the service_path of temporary files on system exit. """ - for file_path in self.service_path.iterdir(): - delete_file(file_path) - audio_path = self.service_path / 'audio' - if audio_path.exists(): - shutil.rmtree(audio_path, True) + for file_name in os.listdir(self.service_path): + file_path = Path(self.service_path, file_name) + if os.path.isdir(file_path): + shutil.rmtree(file_path, True) + else: + delete_file(file_path) def on_theme_combo_box_selected(self, current_index): """ diff --git a/openlp/plugins/images/lib/db.py b/openlp/plugins/images/lib/db.py index 7215441e0..dcf5ae2cb 100644 --- a/openlp/plugins/images/lib/db.py +++ b/openlp/plugins/images/lib/db.py @@ -64,6 +64,7 @@ def init_schema(url): * id * group_id * file_path + * file_hash """ session, metadata = init_db(url) @@ -78,7 +79,8 @@ def init_schema(url): image_filenames_table = Table('image_filenames', metadata, Column('id', types.Integer(), primary_key=True), Column('group_id', types.Integer(), ForeignKey('image_groups.id'), default=None), - Column('file_path', PathType(), nullable=False) + Column('file_path', PathType(), nullable=False), + Column('file_hash', types.Unicode(128), nullable=False) ) mapper(ImageGroups, image_groups_table) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index c8ddc42c0..a3d06bc61 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -24,7 +24,7 @@ from pathlib import Path from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import delete_file, get_images_filter +from openlp.core.common import delete_file, get_images_filter, sha256_file_hash from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, get_natural_key, translate from openlp.core.common.path import create_paths @@ -345,7 +345,7 @@ class ImageMediaItem(MediaManagerItem): :rtype: Path """ ext = image.file_path.suffix.lower() - return self.service_path / '{name:d}{ext}'.format(name=image.id, ext=ext) + return self.service_path / '{name:s}{ext}'.format(name=image.file_hash, ext=ext) def load_full_list(self, images, initial_load=False, open_group=None): """ @@ -490,6 +490,7 @@ class ImageMediaItem(MediaManagerItem): image_file = ImageFilenames() image_file.group_id = group_id image_file.file_path = image_path + image_file.file_hash = sha256_file_hash(image_path) self.manager.save_object(image_file) self.main_window.increment_progress_bar() if reload_list and image_paths: diff --git a/openlp/plugins/images/lib/upgrade.py b/openlp/plugins/images/lib/upgrade.py index 5ce016ef6..ef4620ca0 100644 --- a/openlp/plugins/images/lib/upgrade.py +++ b/openlp/plugins/images/lib/upgrade.py @@ -25,16 +25,17 @@ import json import logging from pathlib import Path -from sqlalchemy import Column, Table +from sqlalchemy import Column, Table, types +from openlp.core.common import sha256_file_hash from openlp.core.common.applocation import AppLocation from openlp.core.common.db import drop_columns -from openlp.core.common.json import OpenLPJSONEncoder +from openlp.core.common.json import OpenLPJSONEncoder, OpenLPJSONDecoder from openlp.core.lib.db import PathType, get_upgrade_op log = logging.getLogger(__name__) -__version__ = 2 +__version__ = 3 def upgrade_1(session, metadata): @@ -67,3 +68,21 @@ def upgrade_2(session, metadata): else: op.drop_constraint('image_filenames', 'foreignkey') op.drop_column('image_filenames', 'filenames') + + +def upgrade_3(session, metadata): + """ + Version 3 upgrade - add sha256 hash + """ + log.debug('Starting upgrade_3 for adding sha256 hashes') + old_table = Table('image_filenames', metadata, autoload=True) + if 'file_hash' not in [col.name for col in old_table.c.values()]: + op = get_upgrade_op(session) + op.add_column('image_filenames', Column('file_hash', types.Unicode(128))) + conn = op.get_bind() + results = conn.execute('SELECT * FROM image_filenames') + for row in results.fetchall(): + file_path = json.loads(row.file_path, cls=OpenLPJSONDecoder) + hash = sha256_file_hash(file_path) + sql = 'UPDATE image_filenames SET file_hash = \'{hash}\' WHERE id = {id}'.format(hash=hash, id=row.id) + conn.execute(sql) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 56065ea40..4797443a7 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -166,20 +166,20 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): 'The optical disc {name} is no longer available.').format(name=name)) return False service_item.processor = 'vlc' + service_item.add_capability(ItemCapabilities.IsOptical) service_item.add_from_command(filename, name, self.clapperboard) service_item.title = clip_name # Set the length service_item.set_media_length(end - start) service_item.start_time = start service_item.end_time = end - service_item.add_capability(ItemCapabilities.IsOptical) elif filename.startswith('devicestream:') or filename.startswith('networkstream:'): # Special handling if the filename is a devicestream (name, mrl, options) = parse_stream_path(filename) service_item.processor = 'vlc' + service_item.add_capability(ItemCapabilities.CanStream) service_item.add_from_command(filename, name, self.clapperboard) service_item.title = name - service_item.add_capability(ItemCapabilities.CanStream) else: if not os.path.exists(filename): if not remote: diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 4d1c967ba..54a444b46 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -19,9 +19,13 @@ # along with this program. If not, see . # ########################################################################## import logging +import os +import shutil from PyQt5 import QtCore, QtWidgets +from pathlib import Path +from openlp.core.common import sha256_file_hash from openlp.core.common.i18n import UiStrings, get_natural_key, translate from openlp.core.common.path import path_to_str from openlp.core.common.registry import Registry @@ -260,6 +264,37 @@ class PresentationMediaItem(MediaManagerItem): doc.presentation_deleted() doc.close_presentation() + def update_thumbnail_scheme(self, file_path): + """ + Update the thumbnail folder naming scheme to the new sha256 based one. + """ + # TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in + # PresentationDocument.get_thumbnail_folder and PresentationDocument.get_temp_folder is removed + for cidx in self.controllers: + if not self.controllers[cidx].enabled(): + # skip presentation controllers that are not enabled + continue + file_ext = file_path.suffix[1:] + if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports: + doc = self.controllers[cidx].add_document(file_path) + # Check if the file actually exists + if file_path.exists(): + thumb_path = doc.get_thumbnail_folder() + hash = sha256_file_hash(file_path) + # Rename the thumbnail folder so that it uses the sha256 naming scheme + if thumb_path.exists(): + new_folder = Path(os.path.split(thumb_path)[0]) / hash + log.info('Moved thumbnails from {md5} to {sha256}'.format(md5=str(thumb_path), + sha256=str(new_folder))) + shutil.move(thumb_path, new_folder) + # Rename the data folder, if one exists + old_folder = doc.get_temp_folder() + if old_folder.exists(): + new_folder = Path(os.path.split(old_folder)[0]) / hash + log.info('Moved data from {md5} to {sha256}'.format(md5=str(old_folder), + sha256=str(new_folder))) + shutil.move(old_folder, new_folder) + def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service, file_path=None, **kwargs): """ @@ -310,12 +345,13 @@ class PresentationMediaItem(MediaManagerItem): image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i) thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i) while image_path.is_file(): - service_item.add_from_image(image_path, file_name, thumbnail=str(thumbnail_path)) + service_item.add_from_image(image_path, file_name, thumbnail=thumbnail_path) i += 1 image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i) thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i) service_item.add_capability(ItemCapabilities.HasThumbnails) doc.close_presentation() + service_item.validate_item() return True else: # File is no longer present @@ -358,10 +394,12 @@ class PresentationMediaItem(MediaManagerItem): note = '' if notes and len(notes) >= i: note = notes[i - 1] - service_item.add_from_command(str(path), file_name, str(thumbnail_path), title, note) + service_item.add_from_command(str(path), file_name, thumbnail_path, title, note, + doc.get_sha256_file_hash()) i += 1 thumbnail_path = doc.get_thumbnail_path(i, True) doc.close_presentation() + service_item.validate_item() return True else: # File is no longer present diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 0afb2d94f..1f1b4a3ea 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -24,11 +24,12 @@ from pathlib import Path from PyQt5 import QtCore -from openlp.core.common import md5_hash +from openlp.core.common import md5_hash, sha256_file_hash from openlp.core.common.applocation import AppLocation from openlp.core.common.path import create_paths from openlp.core.common.registry import Registry -from openlp.core.lib import create_thumb, validate_thumb +from openlp.core.common.settings import Settings +from openlp.core.lib import create_thumb log = logging.getLogger(__name__) @@ -96,6 +97,7 @@ class PresentationDocument(object): :rtype: None """ self.controller = controller + self._sha256_file_hash = None self.settings = Registry().get('settings') self._setup(document_path) @@ -145,6 +147,12 @@ class PresentationDocument(object): # get_temp_folder and PresentationPluginapp_startup is removed if self.settings.value('presentations/thumbnail_scheme') == 'md5': folder = md5_hash(bytes(self.file_path)) + elif Settings().value('presentations/thumbnail_scheme') == 'sha256file': + if self._sha256_file_hash: + folder = self._sha256_file_hash + else: + self._sha256_file_hash = sha256_file_hash(self.file_path) + folder = self._sha256_file_hash else: folder = self.file_path.name return Path(self.controller.thumbnail_folder, folder) @@ -160,13 +168,20 @@ class PresentationDocument(object): # get_thumbnail_folder and PresentationPluginapp_startup is removed if self.settings.value('presentations/thumbnail_scheme') == 'md5': folder = md5_hash(bytes(self.file_path)) + elif Settings().value('presentations/thumbnail_scheme') == 'sha256file': + if self._sha256_file_hash: + folder = self._sha256_file_hash + else: + self._sha256_file_hash = sha256_file_hash(self.file_path) + folder = self._sha256_file_hash else: folder = self.file_path.name return Path(self.controller.temp_folder, folder) def check_thumbnails(self): """ - Check that the last thumbnail image exists and is valid and are more recent than the powerpoint file. + Check that the last thumbnail image exists and is valid. It is not checked if presentation file is newer than + thumbnail since the path is based on the file hash, so if it exists it is by definition up to date. :return: If the thumbnail is valid :rtype: bool @@ -174,7 +189,7 @@ class PresentationDocument(object): last_image_path = self.get_thumbnail_path(self.get_slide_count(), True) if not (last_image_path and last_image_path.is_file()): return False - return validate_thumb(Path(self.file_path), Path(last_image_path)) + return True def close_presentation(self): """ @@ -359,6 +374,17 @@ class PresentationDocument(object): notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) notes_path.write_text(note) + def get_sha256_file_hash(self): + """ + Returns the sha256 file hash for the file. + + :return: The sha256 file hash + :rtype: str + """ + if not self._sha256_file_hash: + self._sha256_file_hash = sha256_file_hash(self.file_path) + return self._sha256_file_hash + class PresentationController(object): """ diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 6123bc29a..46df4580b 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -137,7 +137,11 @@ class PresentationPlugin(Plugin): for path in presentation_paths: self.media_item.clean_up_thumbnails(path, clean_for_update=True) self.media_item.list_view.clear() - self.settings.setValue('presentations/thumbnail_scheme', 'md5') + # Update the thumbnail scheme if needed + if self.settings.value('presentations/thumbnail_scheme') != 'sha256file': + for path in presentation_paths: + self.media_item.update_thumbnail_scheme(path) + self.settings.setValue('presentations/thumbnail_scheme', 'sha256file') self.media_item.validate_and_load(presentation_paths) @staticmethod diff --git a/tests/functional/openlp_core/lib/test_serviceitem.py b/tests/functional/openlp_core/lib/test_serviceitem.py index 244c50bc1..effde5cd9 100644 --- a/tests/functional/openlp_core/lib/test_serviceitem.py +++ b/tests/functional/openlp_core/lib/test_serviceitem.py @@ -26,7 +26,7 @@ import pytest from pathlib import Path from unittest.mock import Mock, MagicMock, patch -from openlp.core.common import ThemeLevel, md5_hash +from openlp.core.common import ThemeLevel from openlp.core.common.enum import ServiceItemType from openlp.core.common.registry import Registry from openlp.core.lib.formattingtags import FormattingTags @@ -75,7 +75,7 @@ def service_item_env(state): Registry().register('image_manager', MagicMock()) -def test_service_item_basic(): +def test_service_item_basic(settings): """ Test the Service Item - basic test """ @@ -123,7 +123,7 @@ def test_service_item_load_image_from_service(state_media, settings): # GIVEN: A new service item and a mocked add icon function image_name = 'image_1.jpg' test_file = TEST_PATH / image_name - frame_array = {'path': test_file, 'title': image_name} + frame_array = {'path': test_file, 'title': image_name, 'file_hash': 'abcd'} service_item = ServiceItem(None) service_item.add_icon = MagicMock() @@ -131,8 +131,9 @@ def test_service_item_load_image_from_service(state_media, settings): # WHEN: adding an image from a saved Service and mocked exists line = convert_file_service_item(TEST_PATH, 'serviceitem_image_1.osj') with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists,\ - patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \ - mocked_get_section_data_path: + patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as mocked_get_section_data_path,\ + patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash: + mocked_sha256_file_hash.return_value = 'abcd' mocked_exists.return_value = True mocked_get_section_data_path.return_value = Path('/path/') service_item.set_from_service(line, TEST_PATH) @@ -169,8 +170,8 @@ def test_service_item_load_image_from_local_service(mocked_get_section_data_path image_name2 = 'image_2.jpg' test_file1 = os.path.join('/home', 'openlp', image_name1) test_file2 = os.path.join('/home', 'openlp', image_name2) - frame_array1 = {'path': test_file1, 'title': image_name1} - frame_array2 = {'path': test_file2, 'title': image_name2} + frame_array1 = {'path': test_file1, 'title': image_name1, 'file_hash': 'abcd'} + frame_array2 = {'path': test_file2, 'title': image_name2, 'file_hash': 'abcd'} service_item = ServiceItem(None) service_item.add_icon = MagicMock() service_item2 = ServiceItem(None) @@ -179,8 +180,10 @@ def test_service_item_load_image_from_local_service(mocked_get_section_data_path # WHEN: adding an image from a saved Service and mocked exists line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj') line2 = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj', 1) - service_item2.set_from_service(line2) - service_item.set_from_service(line) + with patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash: + mocked_sha256_file_hash.return_value = 'abcd' + service_item2.set_from_service(line2) + service_item.set_from_service(line) # THEN: We should get back a valid service item assert service_item.is_valid is True, 'The first service item should be valid' @@ -226,7 +229,9 @@ def test_add_from_command_for_a_presentation(): 'display_title': display_title, 'notes': notes, 'thumbnail': image} # WHEN: adding presentation to service_item - service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes) + with patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash: + mocked_sha256_file_hash.return_value = 'abcd' + service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes) # THEN: verify that it is setup as a Command and that the frame data matches assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command' @@ -245,7 +250,9 @@ def test_add_from_command_without_display_title_and_notes(): 'display_title': None, 'notes': None, 'thumbnail': image} # WHEN: adding image to service_item - service_item.add_from_command(TEST_PATH, image_name, image) + with patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash: + mocked_sha256_file_hash.return_value = 'abcd' + service_item.add_from_command(TEST_PATH, image_name, image) # THEN: verify that it is setup as a Command and that the frame data matches assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command' @@ -268,13 +275,14 @@ def test_add_from_command_for_a_presentation_thumb(mocked_get_section_data_path, thumb = Path('tmp') / 'test' / 'thumb.png' display_title = 'DisplayTitle' notes = 'Note1\nNote2\n' - expected_thumb_path = Path('mocked') / 'section' / 'path' / 'thumbnails' / \ - md5_hash(str(TEST_PATH / presentation_name).encode('utf8')) / 'thumb.png' - frame = {'title': presentation_name, 'image': str(expected_thumb_path), 'path': str(TEST_PATH), - 'display_title': display_title, 'notes': notes, 'thumbnail': str(expected_thumb_path)} + expected_thumb_path = Path('mocked') / 'section' / 'path' / 'thumbnails' / 'abcd' / 'thumb.png' + frame = {'title': presentation_name, 'image': expected_thumb_path, 'path': str(TEST_PATH), + 'display_title': display_title, 'notes': notes, 'thumbnail': expected_thumb_path} # WHEN: adding presentation to service_item - service_item.add_from_command(str(TEST_PATH), presentation_name, thumb, display_title, notes) + with patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash: + mocked_sha256_file_hash.return_value = 'abcd' + service_item.add_from_command(str(TEST_PATH), presentation_name, thumb, display_title, notes) # THEN: verify that it is setup as a Command and that the frame data matches assert service_item.service_item_type == ServiceItemType.Command, 'It should be a Command' @@ -292,7 +300,9 @@ def test_service_item_load_optical_media_from_service(state_media): # WHEN: We load a serviceitem with optical media line = convert_file_service_item(TEST_PATH, 'serviceitem-dvd.osj') - with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists: + with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists,\ + patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash: + mocked_sha256_file_hash.return_value = 'abcd' mocked_exists.return_value = True service_item.set_from_service(line) diff --git a/tests/functional/openlp_plugins/images/test_mediaitem.py b/tests/functional/openlp_plugins/images/test_mediaitem.py index decb33bb7..ca8d6618f 100644 --- a/tests/functional/openlp_plugins/images/test_mediaitem.py +++ b/tests/functional/openlp_plugins/images/test_mediaitem.py @@ -56,12 +56,15 @@ def _recursively_delete_group_side_effect(*args, **kwargs): returned_object1 = ImageFilenames() returned_object1.id = 1 returned_object1.file_path = Path('/', 'tmp', 'test_file_1.jpg') + returned_object1.file_hash = 'abcd1' returned_object2 = ImageFilenames() returned_object2.id = 2 returned_object2.file_path = Path('/', 'tmp', 'test_file_2.jpg') + returned_object2.file_hash = 'abcd2' returned_object3 = ImageFilenames() returned_object3.id = 3 returned_object3.file_path = Path('/', 'tmp', 'test_file_3.jpg') + returned_object3.file_hash = 'abcd3' return [returned_object1, returned_object2, returned_object3] if args[0] == ImageGroups and args[1]: # Change the parent_id that is matched so we don't get into an endless loop @@ -91,7 +94,8 @@ def test_save_new_images_list_empty_list(mocked_load_full_list, media_item): @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') -def test_save_new_images_list_single_image_with_reload(mocked_load_full_list, media_item): +@patch('openlp.plugins.images.lib.mediaitem.sha256_file_hash') +def test_save_new_images_list_single_image_with_reload(mocked_sha256_file_hash, mocked_load_full_list, media_item): """ Test that the save_new_images_list() calls load_full_list() when reload_list is set to True """ @@ -99,6 +103,7 @@ def test_save_new_images_list_single_image_with_reload(mocked_load_full_list, me image_list = [Path('test_image.jpg')] ImageFilenames.file_path = None media_item.manager = MagicMock() + mocked_sha256_file_hash.return_value = 'abcd' # WHEN: We run save_new_images_list with reload_list=True media_item.save_new_images_list(image_list, reload_list=True) @@ -111,13 +116,15 @@ def test_save_new_images_list_single_image_with_reload(mocked_load_full_list, me @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') -def test_save_new_images_list_single_image_without_reload(mocked_load_full_list, media_item): +@patch('openlp.plugins.images.lib.mediaitem.sha256_file_hash') +def test_save_new_images_list_single_image_without_reload(mocked_sha256_file_hash, mocked_load_full_list, media_item): """ Test that the save_new_images_list() doesn't call load_full_list() when reload_list is set to False """ # GIVEN: A list with 1 image and a mocked out manager image_list = [Path('test_image.jpg')] media_item.manager = MagicMock() + mocked_sha256_file_hash.return_value = 'abcd' # WHEN: We run save_new_images_list with reload_list=False media_item.save_new_images_list(image_list, reload_list=False) @@ -127,13 +134,15 @@ def test_save_new_images_list_single_image_without_reload(mocked_load_full_list, @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') -def test_save_new_images_list_multiple_images(mocked_load_full_list, media_item): +@patch('openlp.plugins.images.lib.mediaitem.sha256_file_hash') +def test_save_new_images_list_multiple_images(mocked_sha256_file_hash, mocked_load_full_list, media_item): """ Test that the save_new_images_list() saves all images in the list """ # GIVEN: A list with 3 images image_list = [Path('test_image_1.jpg'), Path('test_image_2.jpg'), Path('test_image_3.jpg')] media_item.manager = MagicMock() + mocked_sha256_file_hash.return_value = 'abcd' # WHEN: We run save_new_images_list with the list of 3 images media_item.save_new_images_list(image_list, reload_list=False) @@ -144,13 +153,15 @@ def test_save_new_images_list_multiple_images(mocked_load_full_list, media_item) @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') -def test_save_new_images_list_other_objects_in_list(mocked_load_full_list, media_item): +@patch('openlp.plugins.images.lib.mediaitem.sha256_file_hash') +def test_save_new_images_list_other_objects_in_list(mocked_sha256_file_hash, mocked_load_full_list, media_item): """ Test that the save_new_images_list() ignores everything in the provided list except strings """ # GIVEN: A list with images and objects image_list = [Path('test_image_1.jpg'), None, True, ImageFilenames(), Path('test_image_2.jpg')] media_item.manager = MagicMock() + mocked_sha256_file_hash.return_value = 'abcd' # WHEN: We run save_new_images_list with the list of images and objects media_item.save_new_images_list(image_list, reload_list=False) @@ -177,7 +188,8 @@ def test_on_reset_click(media_item): @patch('openlp.plugins.images.lib.mediaitem.delete_file') -def test_recursively_delete_group(mocked_delete_file, media_item): +@patch('openlp.core.lib.serviceitem.sha256_file_hash') +def test_recursively_delete_group(mocked_sha256_file_hash, mocked_delete_file, media_item): """ Test that recursively_delete_group() works """ @@ -189,6 +201,7 @@ def test_recursively_delete_group(mocked_delete_file, media_item): media_item.service_path = Path() test_group = ImageGroups() test_group.id = 1 + mocked_sha256_file_hash.return_value = 'abcd' # WHEN: recursively_delete_group() is called media_item.recursively_delete_group(test_group) @@ -205,7 +218,8 @@ def test_recursively_delete_group(mocked_delete_file, media_item): @patch('openlp.plugins.images.lib.mediaitem.delete_file') @patch('openlp.plugins.images.lib.mediaitem.check_item_selected') -def test_on_delete_click(mocked_check_item_selected, mocked_delete_file, media_item): +@patch('openlp.core.lib.serviceitem.sha256_file_hash') +def test_on_delete_click(mocked_sha256_file_hash, mocked_check_item_selected, mocked_delete_file, media_item): """ Test that on_delete_click() works """ @@ -215,6 +229,7 @@ def test_on_delete_click(mocked_check_item_selected, mocked_delete_file, media_i test_image.id = 1 test_image.group_id = 1 test_image.file_path = Path('imagefile.png') + test_image.file_hash = 'abcd' media_item.manager = MagicMock() media_item.service_path = Path() media_item.list_view = MagicMock() @@ -222,6 +237,7 @@ def test_on_delete_click(mocked_check_item_selected, mocked_delete_file, media_i mocked_row_item.data.return_value = test_image mocked_row_item.text.return_value = '' media_item.list_view.selectedItems.return_value = [mocked_row_item] + mocked_sha256_file_hash.return_value = 'abcd' # WHEN: Calling on_delete_click media_item.on_delete_click() diff --git a/tests/functional/openlp_plugins/images/test_upgrade.py b/tests/functional/openlp_plugins/images/test_upgrade.py index d36266f77..823fff2fb 100644 --- a/tests/functional/openlp_plugins/images/test_upgrade.py +++ b/tests/functional/openlp_plugins/images/test_upgrade.py @@ -57,11 +57,13 @@ def test_image_filenames_table(db_url, settings): Test that the ImageFilenames table is correctly upgraded to the latest version """ # GIVEN: An unversioned image database - with patch.object(AppLocation, 'get_data_path', return_value=Path('/', 'test', 'dir')): + with patch.object(AppLocation, 'get_data_path', return_value=Path('/', 'test', 'dir')),\ + patch('openlp.plugins.images.lib.upgrade.sha256_file_hash') as mocked_sha256_file_hash: + mocked_sha256_file_hash.return_value = 'abcd' # WHEN: Initalising the database manager upgrade_db(db_url, upgrade) engine = create_engine(db_url) conn = engine.connect() - assert conn.execute('SELECT * FROM metadata WHERE key = "version"').first().value == '2' + assert conn.execute('SELECT * FROM metadata WHERE key = "version"').first().value == '3' diff --git a/tests/interfaces/openlp_core/widgets/test_views.py b/tests/interfaces/openlp_core/widgets/test_views.py index 0e72b7e8f..d5763b085 100644 --- a/tests/interfaces/openlp_core/widgets/test_views.py +++ b/tests/interfaces/openlp_core/widgets/test_views.py @@ -67,7 +67,7 @@ def test_replace_service_item(preview_widget, state_media): # GIVEN: A ServiceItem with two frames. service_item = ServiceItem(None) service = read_service_from_file('serviceitem_image_3.osj') - with patch('os.path.exists'): + with patch('os.path.exists') and patch('openlp.core.lib.serviceitem.sha256_file_hash'): service_item.set_from_service(service[0]) # WHEN: Added to the preview widget. preview_widget.replace_service_item(service_item, 1, 1) @@ -83,7 +83,7 @@ def test_change_slide(preview_widget, state_media): # GIVEN: A ServiceItem with two frames content. service_item = ServiceItem(None) service = read_service_from_file('serviceitem_image_3.osj') - with patch('os.path.exists'): + with patch('os.path.exists') and patch('openlp.core.lib.serviceitem.sha256_file_hash'): service_item.set_from_service(service[0]) # WHEN: Added to the preview widget and switched to the second frame. preview_widget.replace_service_item(service_item, 1, 0)