diff options
Diffstat (limited to 'raphodo/problemnotification.py')
-rwxr-xr-x | raphodo/problemnotification.py | 599 |
1 files changed, 599 insertions, 0 deletions
diff --git a/raphodo/problemnotification.py b/raphodo/problemnotification.py new file mode 100755 index 0000000..787b0bb --- /dev/null +++ b/raphodo/problemnotification.py @@ -0,0 +1,599 @@ +# Copyright (C) 2010-2017 Damon Lynch <damonlynch@gmail.com> + +# This file is part of Rapid Photo Downloader. +# +# Rapid Photo Downloader is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rapid Photo Downloader is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rapid Photo Downloader. If not, +# see <http://www.gnu.org/licenses/>. + +""" +Notify user of problems when downloading: problems with subfolder and filename generation, +download errors, and so forth + +Goals +===== + +Group problems into tasks: + 1. scanning + 2. copying + 3. renaming (presented to user as finalizing file and download subfolder names) + 4. backing up - per backup device + +Present messages in human readable manner. +Multiple metadata problems can occur: group them. +Distinguish error severity + +""" + +__author__ = 'Damon Lynch' +__copyright__ = "Copyright 2010-2017, Damon Lynch" + +from collections import deque +from typing import Tuple, Optional, List, Union, Iterator +from html import escape +from gettext import gettext as _ + +import logging + +from raphodo.utilities import make_internationalized_list +from raphodo.constants import ErrorType + + +def make_href(name: str, uri: str) -> str: + """ + Construct a hyperlink. + """ + + # Note: keep consistent with ErrorReport._saveUrls() + return '<a href="{}">{}</a>'.format(uri, escape(name)) + + +class Problem: + def __init__(self, name: Optional[str]=None, + uri: Optional[str]=None, + exception: Optional[Exception]=None, + **attrs) -> None: + for attr, value in attrs.items(): + setattr(self, attr, value) + self.name = name + self.uri = uri + self.exception = exception + + @property + def title(self) -> str: + logging.critical('title() not implemented in subclass %s', self.__class__.__name__) + return 'undefined' + + @property + def body(self) -> str: + logging.critical('body() not implemented in subclass %s', self.__class__.__name__) + return 'undefined' + + @property + def details(self) -> List[str]: + if self.exception is not None: + try: + return [escape(_("Error: %(errno)s %(strerror)s")) % dict( + errno=self.exception.errno, strerror=self.exception.strerror)] + except AttributeError: + return [escape(_("Error: %s")) % self.exception] + else: + return [] + + @property + def href(self) -> str: + if self.name and self.uri: + return make_href(name=self.name, uri=self.uri) + else: + logging.critical('href() is missing name or uri in subclass %s', + self.__class__.__name__) + + @property + def severity(self) -> ErrorType: + return ErrorType.warning + + +class SeriousProblem(Problem): + @property + def severity(self) -> ErrorType: + return ErrorType.serious_error + + +class CameraGpProblem(SeriousProblem): + @property + def details(self) -> List[str]: + try: + return [escape(_("GPhoto2 Error: %s")) % self.gp_code] + except AttributeError: + return [] + + +class CameraInitializationProblem(CameraGpProblem): + @property + def body(self) -> str: + return escape(_("Unable to initialize the camera, probably because another program is " + "using it. No files were copied from it.")) + @property + def severity(self) -> ErrorType: + return ErrorType.critical_error + + +class CameraDirectoryReadProblem(CameraGpProblem): + @property + def body(self) -> str: + return escape(_("Unable to read directory %s")) % self.href + + +class CameraFileInfoProblem(CameraGpProblem): + @property + def body(self) -> str: + return escape(_('Unable to access modification time or size from %s')) % self.href + + +class CameraFileReadProblem(CameraGpProblem): + @property + def body(self) -> str: + return escape(_('Unable to read file %s')) % self.href + + +class FileWriteProblem(SeriousProblem): + @property + def body(self) -> str: + return escape(_('Unable to write file %s')) % self.href + + +class FileMoveProblem(SeriousProblem): + @property + def body(self) -> str: + return escape(_('Unable to move file %s')) % self.href + + +class FileDeleteProblem(SeriousProblem): + @property + def body(self) -> str: + return escape(_('Unable to remove file %s')) % self.href + + +class FileCopyProblem(SeriousProblem): + @property + def body(self) -> str: + return escape(_('Unable to copy file %s')) % self.href + + +class FileZeroLengthProblem(SeriousProblem): + @property + def body(self) -> str: + return escape(_('Zero length file %s will not be downloaded')) % self.href + + +class FsMetadataReadProblem(Problem): + @property + def body(self) -> str: + return escape(_("Could not determine filesystem modification time for %s")) % self.href + + +class FileMetadataLoadProblem(Problem): + @property + def body(self) -> str: + return escape(_('Unable to load metadata from %s')) % self.href + + +class FileMetadataLoadProblemNoDownload(SeriousProblem): + @property + def body(self) -> str: + return escape(_('Unable to load metadata from %(name)s. The %(filetype)s was not ' + 'downloaded.')) % dict(filetype=self.file_type, name=self.href) + + +class FsMetadataWriteProblem(Problem): + @property + def body(self) -> str: + return escape(_( + "An error occurred setting a file's filesystem metadata on the filesystem %s. " + "If this error occurs again on the same filesystem, it will not be reported again." + )) % self.href + + @property + def details(self) -> List[str]: + return [escape(_("Error: %(errno)s %(strerror)s")) % dict(errno=e.errno, + strerror=e.strerror) + for e in self.mdata_exceptions] + + +class UnhandledFileProblem(SeriousProblem): + @property + def body(self) -> str: + return escape(_('Encountered unhandled file %s. It will not be downloaded.')) % self.href + + +class FileAlreadyExistsProblem(SeriousProblem): + @property + def body(self) -> str: + return escape( + _("%(filetype)s %(destination)s already exists.") + ) % dict( + filetype=escape(self.file_type_capitalized), + destination=self.href + ) + + @property + def details(self) -> List[str]: + d = list() + d.append( + escape( + _("The existing %(filetype)s %(destination)s was last modified on " + "%(date)s at %(time)s.") + ) % dict( + filetype=escape(self.file_type), + date=escape(self.date), + time=escape(self.time), + destination=self.href + ) + ) + d.append( + escape( + _("The %(filetype)s %(source)s was not downloaded from %(device)s.") + ) % dict( + filetype=escape(self.file_type), + source=self.source, + device=self.device + ) + ) + return d + + +class IdentifierAddedProblem(FileAlreadyExistsProblem): + + @property + def details(self) -> List[str]: + d = list() + d.append( + escape( + _("The existing %(filetype)s %(destination)s was last modified on " + "%(date)s at %(time)s.") + ) % dict( + filetype=escape(self.file_type), + date=escape(self.date), + time=escape(self.time), + destination=self.href + ) + ) + d.append( + escape( + _("The %(filetype)s %(source)s was downloaded from %(device)s.") + ) % dict( + filetype=escape(self.file_type), + source=self.source, + device=self.device + ) + ) + d.append( + escape( + _("The unique identifier '%s' was added to the filename.")) % self.identifier + ) + return d + + @property + def severity(self) -> ErrorType: + return ErrorType.warning + + +class BackupAlreadyExistsProblem(FileAlreadyExistsProblem): + + @property + def details(self) -> List[str]: + d = list() + d.append( + escape( + _("The existing backup %(filetype)s %(destination)s was last modified on " + "%(date)s at %(time)s.") + ) % dict( + filetype=escape(self.file_type), + date=escape(self.date), + time=escape(self.time), + destination=self.href + ) + ) + d.append( + escape( + _("The %(filetype)s %(source)s was not backed up from %(device)s.") + ) % dict( + filetype=escape(self.file_type), + source=self.source, + device=self.device + ) + ) + return d + + +class BackupOverwrittenProblem(BackupAlreadyExistsProblem): + + @property + def details(self) -> List[str]: + d = list() + d.append( + escape( + _("The previous backup %(filetype)s %(destination)s was last modified on " + "%(date)s at %(time)s.") + ) % dict( + filetype=escape(self.file_type), + date=escape(self.date), + time=escape(self.time), + destination=self.name + ) + ) + d.append( + escape( + _("The %(filetype)s %(source)s from %(device)s was backed up, overwriting the " + "previous backup %(filetype)s.") + ) % dict( + filetype=escape(self.file_type), + source=self.source, + device=self.device + ) + ) + return d + + @property + def severity(self) -> ErrorType: + return ErrorType.warning + + +class DuplicateFileWhenSyncingProblem(SeriousProblem): + @property + def body(self) -> str: + return escape( + _("When synchronizing RAW + JPEG sequence values, a duplicate %(filetype)s " + "%(file)s was encountered, and was not downloaded." + ) + ) % dict(file=self.href, filetype=self.file_type) + + +class SameNameDifferentExif(Problem): + @property + def body(self) -> str: + return escape( + _("When synchronizing RAW + JPEG sequence values, photos were detected with the " + "same filenames, but taken at different times:") + ) + + @property + def details(self) -> List[str]: + return [escape( + _("%(image1)s was taken on %(image1_date)s at %(image1_time)s, and %(image2)s " + "on %(image2_date)s at %(image2_time)s.") + ) % dict( + image1=self.image1, + image1_date=self.image1_date, + image1_time=self.image1_time, + image2=self.image2, + image2_date=self.image2_date, + image2_time=self.image2_time + )] + + +class RenamingAssociateFileProblem(SeriousProblem): + @property + def body(self) -> str: + return escape( + _("Unable to finalize the filename for %s") + ) % self.source + + +class FilenameNotFullyGeneratedProblem(Problem): + def __init__(self, name: Optional[str]=None, + uri: Optional[str]=None, + exception: Optional[Exception]=None, + **attrs) -> None: + super().__init__(name=name, uri=uri, exception=exception, **attrs) + self.missing_metadata = [] + self.file_type = '' + self.destination = '' + self.source = '' + self.bad_converstion_date_time = False + self.bad_conversion_exception = None # type: Optional[Exception] + self.invalid_date_time = False + self.missing_extension = False + self.missing_image_no = False + self.component_error = False + self.component_problem = '' + self.component_exception = None + + def has_error(self) -> bool: + """ + :return: True if any of the errors occurred + """ + + return bool(self.missing_metadata) or self.invalid_date_time or \ + self.bad_converstion_date_time or self.missing_extension or self.missing_image_no \ + or self.component_error + + @property + def body(self) -> str: + return escape( + _("The filename %(destination)s was not fully generated for %(filetype)s %(source)s.") + ) % dict(destination=self.destination, filetype=self.file_type, source=self.source) + + @property + def details(self) -> List[str]: + d = [] + if len(self.missing_metadata) == 1: + d.append( + escape( + _("The %(type)s metadata is missing.") + ) % dict(type=self.missing_metadata[0]) + ) + elif len(self.missing_metadata) > 1: + d.append( + escape( + _("The following metadata is missing: %s.") + ) % make_internationalized_list(self.missing_metadata) + ) + + if self.bad_converstion_date_time: + d.append( + escape(_('Date/time conversion failed: %s.')) % self.bad_conversion_exception + ) + + if self.invalid_date_time: + d.append( + escape( + _("Could not extract valid date/time metadata or determine the file " + "modification time.") + ) + ) + + if self.missing_extension: + d.append(escape(_("Filename does not have an extension."))) + + if self.missing_image_no: + d.append(escape(_("Filename does not have a number component."))) + + if self.component_error: + d.append( + escape(_("Error generating component %(component)s. Error: %(error)s")) % dict( + component=self.component_problem, + error=self.component_exception + ) + ) + + return d + + +class FolderNotFullyGeneratedProblemProblem(FilenameNotFullyGeneratedProblem): + @property + def body(self) -> str: + return escape( + _("The download subfolders %(folder)s were only partially generated for %(filetype)s " + "%(source)s.") + ) % dict(folder=self.destination, filetype=self.file_type, source=self.source) + + +class NoDataToNameProblem(SeriousProblem): + @property + def body(self) -> str: + return escape( + _("There is no data with which to generate the %(subfolder_file)s for %(filename)s. " + "The %(filetype)s was not downloaded.") + ) % dict( + subfolder_file = self.area, + filename = self.href, + filetype=self.file_type, + ) + + +class RenamingFileProblem(SeriousProblem): + @property + def body(self) -> str: + return escape( + _('Unable to create the %(filetype)s %(destination)s in %(folder)s. The download file ' + 'was %(source)s in %(device)s. It was not downloaded.') + ) % dict( + filetype=escape(self.file_type), + destination=escape(self.destination), + folder=self.folder, + source=self.href, + device=self.device + ) + + +class SubfolderCreationProblem(Problem): + @property + def body(self) -> str: + return escape( + _('Unable to create the download subfolder %s.') + ) % self.folder + + @property + def severity(self) -> ErrorType: + return ErrorType.critical_error + + +class BackupSubfolderCreationProblem(SubfolderCreationProblem): + @property + def body(self) -> str: + return escape( + _('Unable to create the backup subfolder %s.') + ) % self.folder + + +class Problems: + def __init__(self, name: Optional[str]='', + uri: Optional[str]='', + problem: Optional[Problem]=None) -> None: + self.problems = deque() + self.name = name + self.uri = uri + if problem: + self.append(problem=problem) + + def __len__(self) -> int: + return len(self.problems) + + def __iter__(self) -> Iterator[Problem]: + return iter(self.problems) + + def __getitem__(self, index: int) -> Problem: + return self.problems[index] + + def append(self, problem: Problem) -> None: + self.problems.append(problem) + + @property + def title(self) -> str: + logging.critical('title() not implemented in subclass %s', self.__class__.__name__) + return 'undefined' + + @property + def body(self) -> str: + return 'body' + + @property + def details(self) -> List[str]: + return [] + + @property + def href(self) -> str: + if self.name and self.uri: + return make_href(name=self.name, uri=self.uri) + else: + logging.critical('href() is missing name or uri in %s', self.__class__.__name__) + + +class ScanProblems(Problems): + + @property + def title(self) -> str: + return escape(_('Problems scanning %s')) % self.href + + +class CopyingProblems(Problems): + + @property + def title(self) -> str: + return escape(_('Problems copying from %s')) % self.href + + +class RenamingProblems(Problems): + + @property + def title(self) -> str: + return escape(_('Problems while finalizing filenames and generating subfolders')) + + +class BackingUpProblems(Problems): + + @property + def title(self) -> str: + return escape(_('Problems backing up to %s')) % self.href + |