summaryrefslogtreecommitdiff
path: root/raphodo/problemnotification.py
diff options
context:
space:
mode:
Diffstat (limited to 'raphodo/problemnotification.py')
-rwxr-xr-xraphodo/problemnotification.py599
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
+