New service file format

This commit is contained in:
Tomas Groth 2020-05-07 05:18:37 +00:00 committed by Raoul Snyman
parent d2881ad6e1
commit 937c6c3e81
15 changed files with 364 additions and 97 deletions

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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):
"""

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -19,9 +19,13 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
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

View File

@ -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):
"""

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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'

View File

@ -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)