summaryrefslogtreecommitdiff
path: root/raphodo/backuppanel.py
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff-webhosting.net>2018-01-04 08:57:25 +0100
committerJörg Frings-Fürst <debian@jff-webhosting.net>2018-01-04 08:57:25 +0100
commit8ce494b17065c724187dd3f9faec1e419496f871 (patch)
treefa0c7fb1296f30bfd0cdc241c7556cec8d1e8ba1 /raphodo/backuppanel.py
parent18afe3e2ebdb10bbc542d79280344d9adf923d2f (diff)
parenteba0a9bd6f142cdb299cc070060723d00e81205f (diff)
Merge branch 'feature/upstream' into develop
Diffstat (limited to 'raphodo/backuppanel.py')
-rw-r--r--raphodo/backuppanel.py788
1 files changed, 788 insertions, 0 deletions
diff --git a/raphodo/backuppanel.py b/raphodo/backuppanel.py
new file mode 100644
index 0000000..eb823f8
--- /dev/null
+++ b/raphodo/backuppanel.py
@@ -0,0 +1,788 @@
+# Copyright (C) 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/>.
+
+"""
+Display backup preferences
+"""
+
+__author__ = 'Damon Lynch'
+__copyright__ = "Copyright 2017, Damon Lynch"
+
+from typing import Optional, Dict, Tuple, Union, Set, List, DefaultDict
+import logging
+import os
+from collections import namedtuple, defaultdict
+
+from gettext import gettext as _
+
+
+from PyQt5.QtCore import (Qt, pyqtSlot, QAbstractListModel, QModelIndex, QSize)
+from PyQt5.QtWidgets import (
+ QWidget, QSizePolicy, QVBoxLayout, QLabel, QLineEdit, QCheckBox, QScrollArea, QFrame,
+ QStyledItemDelegate, QStyleOptionViewItem, QStyle, QGroupBox, QHBoxLayout, QGridLayout
+)
+from PyQt5.QtGui import (QPainter, QFontMetrics, QFont, QColor, QPalette, QIcon)
+
+from raphodo.constants import (
+ StandardFileLocations, ThumbnailBackgroundName, FileType, Roles, ViewRowType,
+ BackupLocationType
+)
+from raphodo.viewutils import (QFramedWidget, RowTracker)
+from raphodo.rpdfile import FileTypeCounter, Photo, Video
+from raphodo.panelview import QPanelView
+from raphodo.preferences import Preferences
+from raphodo.foldercombo import FolderCombo
+import raphodo.qrc_resources as qrc_resources
+from raphodo.storage import (ValidMounts, get_media_dir, StorageSpace, get_path_display_name)
+from raphodo.devices import (BackupDeviceCollection, BackupVolumeDetails)
+from raphodo.devicedisplay import (DeviceDisplay, BodyDetails, icon_size, DeviceView)
+from raphodo.destinationdisplay import make_body_details, adjusted_download_size
+from raphodo.storage import get_mount_size
+
+
+BackupVolumeUse = namedtuple('BackupVolumeUse', 'bytes_total bytes_free backup_type marked '
+ 'photos_size_to_download videos_size_to_download')
+BackupViewRow = namedtuple('BackupViewRow', 'mount display_name backup_type os_stat_device')
+
+
+class BackupDeviceModel(QAbstractListModel):
+ """
+ Stores 'devices' used for backing up photos and videos.
+
+ Want to display:
+ (1) destination on local files systems
+ (2) external devices, e.g. external hard drives
+
+ Need to account for when download destination is same file system
+ as backup destination.
+ """
+
+ def __init__(self, parent) -> None:
+ super().__init__(parent)
+ self.raidApp = parent.rapidApp
+ self.prefs = parent.prefs
+ size = icon_size()
+ self.removableIcon = QIcon(':icons/drive-removable-media.svg').pixmap(size)
+ self.folderIcon = QIcon(':/icons/folder.svg').pixmap(size)
+ self._initValues()
+
+ def _initValues(self):
+ self.rows = RowTracker() # type: RowTracker
+ self.row_id_counter = 0 # type: int
+ # {row_id}
+ self.headers = set() # type: Set[int]
+ # path: BackupViewRow
+ self.backup_devices = dict() # type: Dict[str, BackupViewRow]
+ self.path_to_row_ids = defaultdict(list) # type: Dict[str, List[int]]
+ self.row_id_to_path = dict() # type: Dict[int, str]
+
+ self.marked = FileTypeCounter()
+ self.photos_size_to_download = self.videos_size_to_download = 0
+
+ # os_stat_device: Set[FileType]
+ self._downloading_to = defaultdict(list) # type: DefaultDict[int, Set[FileType]]
+
+ @property
+ def downloading_to(self):
+ return self._downloading_to
+
+ @downloading_to.setter
+ def downloading_to(self, downloading_to: DefaultDict[int, Set[FileType]]):
+ self._downloading_to = downloading_to
+ self.downloadSizeChanged()
+
+ def reset(self) -> None:
+ self.beginResetModel()
+ self._initValues()
+ self.endResetModel()
+
+ def columnCount(self, parent=QModelIndex()):
+ return 1
+
+ def rowCount(self, parent=QModelIndex()):
+ return max(len(self.rows), 1)
+
+ def insertRows(self, position, rows=2, index=QModelIndex()):
+ self.beginInsertRows(QModelIndex(), position, position + rows - 1)
+ self.endInsertRows()
+ return True
+
+ def removeRows(self, position, rows=2, index=QModelIndex()):
+ self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
+ self.endRemoveRows()
+ return True
+
+ def addBackupVolume(self, mount_details: BackupVolumeDetails) -> None:
+
+ mount = mount_details.mount
+ display_name = mount_details.name
+ path = mount_details.path
+ backup_type = mount_details.backup_type
+ os_stat_device = mount_details.os_stat_device
+
+ assert mount is not None
+ assert display_name
+ assert path
+ assert backup_type
+
+ # two rows per device: header row, and detail row
+ row = len(self.rows)
+ self.insertRows(position=row)
+ logging.debug("Adding %s to backup device display with root path %s at rows %s - %s",
+ display_name, mount.rootPath(), row, row+1)
+
+ for row_id in range(self.row_id_counter, self.row_id_counter + 2):
+ self.row_id_to_path[row_id] = path
+ self.rows[row] = row_id
+ row += 1
+ self.path_to_row_ids[path].append(row_id)
+
+ header_row_id = self.row_id_counter
+ self.headers.add(header_row_id)
+
+ self.row_id_counter += 2
+
+ self.backup_devices[path] = BackupViewRow(mount=mount, display_name=display_name,
+ backup_type=backup_type,
+ os_stat_device=os_stat_device)
+
+ def removeBackupVolume(self, path: str) -> None:
+ """
+ :param path: the value of the volume (mount's path), NOT a
+ manually specified path!
+ """
+
+ row_ids = self.path_to_row_ids[path]
+ header_row_id = row_ids[0]
+ row = self.rows.row(header_row_id)
+ logging.debug("Removing 2 rows from backup view, starting at row %s", row)
+ self.rows.remove_rows(row, 2)
+ self.headers.remove(header_row_id)
+ del self.path_to_row_ids[path]
+ del self.backup_devices[path]
+ for row_id in row_ids:
+ del self.row_id_to_path[row_id]
+ self.removeRows(row, 2)
+
+ def setDownloadAttributes(self, marked: FileTypeCounter,
+ photos_size: int,
+ videos_size: int,
+ merge: bool) -> None:
+ """
+ Set the attributes used to generate the visual display of the
+ files marked to be downloaded
+
+ :param marked: number and type of files marked for download
+ :param photos_size: size in bytes of photos marked for download
+ :param videos_size: size in bytes of videos marked for download
+ :param merge: whether to replace or add to the current values
+ """
+
+ if not merge:
+ self.marked = marked
+ self.photos_size_to_download = photos_size
+ self.videos_size_to_download = videos_size
+ else:
+ self.marked.update(marked)
+ self.photos_size_to_download += photos_size
+ self.videos_size_to_download += videos_size
+ self.downloadSizeChanged()
+
+ def downloadSizeChanged(self) -> None:
+ # TODO possibly optimize for photo vs video rows
+ for row in range(1, len(self.rows), 2):
+ self.dataChanged.emit(self.index(row, 0), self.index(row, 0))
+
+ def _download_size_by_backup_type(self, backup_type: BackupLocationType) -> Tuple[int, int]:
+ """
+ Include photos or videos in download size only if those file types
+ are being backed up to this backup device
+ :param backup_type: which file types are being backed up to this device
+ :return: photos_size_to_download, videos_size_to_download
+ """
+
+ photos_size_to_download = videos_size_to_download = 0
+ if backup_type != BackupLocationType.videos:
+ photos_size_to_download = self.photos_size_to_download
+ if backup_type != BackupLocationType.photos:
+ videos_size_to_download = self.videos_size_to_download
+ return photos_size_to_download, videos_size_to_download
+
+ def data(self, index: QModelIndex, role=Qt.DisplayRole):
+
+ if not index.isValid():
+ return None
+
+ row = index.row()
+
+ # check for special case where no backup devices are active
+ if len(self.rows) == 0:
+ if role == Qt.DisplayRole:
+ return ViewRowType.header
+ else:
+ assert role == Roles.device_details
+ if not self.prefs.backup_files:
+ return (_('Backups are not configured'), self.removableIcon)
+ elif self.prefs.backup_device_autodetection:
+ return (_('No backup devices detected'), self.removableIcon)
+ else:
+ return (_('Valid backup locations not yet specified'), self.folderIcon)
+
+ # at least one device / location is being used
+ if row >= len(self.rows) or row < 0:
+ return None
+ if row not in self.rows:
+ return None
+
+ row_id = self.rows[row]
+ path = self.row_id_to_path[row_id]
+
+ if role == Qt.DisplayRole:
+ if row_id in self.headers:
+ return ViewRowType.header
+ else:
+ return ViewRowType.content
+ else:
+ device = self.backup_devices[path]
+ mount = device.mount
+
+ if role == Qt.ToolTipRole:
+ return path
+ elif role == Roles.device_details:
+ if self.prefs.backup_device_autodetection:
+ icon = self.removableIcon
+ else:
+ icon = self.folderIcon
+ return (device.display_name, icon)
+ elif role == Roles.storage:
+ photos_size_to_download, videos_size_to_download = \
+ self._download_size_by_backup_type(backup_type=device.backup_type)
+
+ photos_size_to_download, videos_size_to_download = adjusted_download_size(
+ photos_size_to_download=photos_size_to_download,
+ videos_size_to_download=videos_size_to_download,
+ os_stat_device=device.os_stat_device,
+ downloading_to=self._downloading_to)
+
+ bytes_total, bytes_free = get_mount_size(mount=mount)
+
+ return BackupVolumeUse(
+ bytes_total=bytes_total,
+ bytes_free=bytes_free,
+ backup_type=device.backup_type,
+ marked = self.marked,
+ photos_size_to_download=photos_size_to_download,
+ videos_size_to_download=videos_size_to_download
+ )
+
+ return None
+
+ def sufficientSpaceAvailable(self) -> bool:
+ """
+ Detect if each backup device has sufficient space for backing up, taking
+ into accoutn situations where downloads and backups are going to the same
+ partition.
+
+ :return: False if any backup device has insufficient space, else True.
+ True if there are no backup devices.
+ """
+
+ for device in self.backup_devices.values():
+ photos_size_to_download, videos_size_to_download = \
+ self._download_size_by_backup_type(backup_type=device.backup_type)
+ photos_size_to_download, videos_size_to_download = adjusted_download_size(
+ photos_size_to_download=photos_size_to_download,
+ videos_size_to_download=videos_size_to_download,
+ os_stat_device=device.os_stat_device,
+ downloading_to=self._downloading_to
+ )
+
+ bytes_total, bytes_free = get_mount_size(mount=device.mount)
+ if photos_size_to_download + videos_size_to_download >= bytes_free:
+ return False
+ return True
+
+
+class BackupDeviceView(DeviceView):
+ def __init__(self, rapidApp, parent=None) -> None:
+ super().__init__(rapidApp, parent)
+ self.setMouseTracking(False)
+ self.entered.disconnect()
+
+
+class BackupDeviceDelegate(QStyledItemDelegate):
+ def __init__(self, rapidApp, parent=None) -> None:
+ super().__init__(parent)
+ self.rapidApp = rapidApp
+ self.deviceDisplay = DeviceDisplay()
+
+ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None:
+ painter.save()
+
+ x = option.rect.x()
+ y = option.rect.y()
+ width = option.rect.width()
+
+ view_type = index.data(Qt.DisplayRole) # type: ViewRowType
+ if view_type == ViewRowType.header:
+ display_name, icon = index.data(Roles.device_details)
+
+ self.deviceDisplay.paint_header(painter=painter, x=x, y=y, width=width,
+ icon=icon,
+ display_name=display_name,
+ )
+ else:
+ assert view_type == ViewRowType.content
+
+ data = index.data(Roles.storage) # type: BackupVolumeUse
+ details = make_body_details(bytes_total=data.bytes_total,
+ bytes_free=data.bytes_free,
+ files_to_display=data.backup_type,
+ marked=data.marked,
+ photos_size_to_download=data.photos_size_to_download,
+ videos_size_to_download=data.videos_size_to_download)
+
+ self.deviceDisplay.paint_body(painter=painter, x=x,
+ y=y,
+ width=width,
+ details=details)
+
+ painter.restore()
+
+ def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
+ view_type = index.data(Qt.DisplayRole) # type: ViewRowType
+ if view_type == ViewRowType.header:
+ height = self.deviceDisplay.device_name_height
+ else:
+ storage_space = index.data(Roles.storage)
+
+ if storage_space is None:
+ height = self.deviceDisplay.base_height
+ else:
+ height = self.deviceDisplay.storage_height
+ return QSize(self.deviceDisplay.view_width, height)
+
+class BackupOptionsWidget(QFramedWidget):
+ """
+ Display and allow editing of preference values for Downloads today
+ and Stored Sequence Number and associated options, as well as
+ the strip incompatible characters option.
+ """
+
+ def __init__(self, prefs: Preferences, parent, rapidApp) -> None:
+ super().__init__(parent)
+
+ self.rapidApp = rapidApp
+ self.prefs = prefs
+ self.media_dir = get_media_dir()
+
+ self.setBackgroundRole(QPalette.Base)
+ self.setAutoFillBackground(True)
+
+ backupLayout = QGridLayout()
+ layout = QVBoxLayout()
+ layout.addLayout(backupLayout)
+ self.setLayout(layout)
+
+ self.backupExplanation = QLabel(
+ _(
+ 'You can have your photos and videos backed up to '
+ 'multiple locations as they are downloaded, e.g. '
+ 'external hard drives.'
+ )
+ )
+ self.backupExplanation.setWordWrap(True)
+ self.backupExplanation.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum)
+
+ self.backup = QCheckBox(_('Back up photos and videos when downloading'))
+ self.backup.setChecked(self.prefs.backup_files)
+ self.backup.stateChanged.connect(self.backupChanged)
+
+ checkbox_width = self.backup.style().pixelMetric(QStyle.PM_IndicatorWidth)
+
+ self.autoBackup = QCheckBox(_('Automatically detect backup devices'))
+ self.autoBackup.setChecked(self.prefs.backup_device_autodetection)
+ self.autoBackup.stateChanged.connect(self.autoBackupChanged)
+
+ self.folderExplanation = QLabel(
+ _(
+ 'Specify the folder in which backups are stored on the '
+ 'device.<br><br>'
+ '<i>Note: the presence of a folder with this name '
+ 'is used to determine if the device is used for backups. '
+ 'For each device you wish to use for backing up to, '
+ 'create a folder in it with one of these folder names. '
+ 'By adding both folders, the same device can be used '
+ 'to back up both photos and videos.</i>'
+ )
+ )
+ self.folderExplanation.setWordWrap(True)
+ self.folderExplanation.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum)
+ # Unless this next call is made, for some reason the widget is too high! :-(
+ self.folderExplanation.setContentsMargins(0, 0, 1, 0)
+
+ self.photoFolderNameLabel = QLabel(_('Photo folder name:'))
+ self.photoFolderName = QLineEdit()
+ self.photoFolderName.setText(self.prefs.photo_backup_identifier)
+ self.photoFolderName.editingFinished.connect(self.photoFolderIdentifierChanged)
+
+ self.videoFolderNameLabel = QLabel(_('Video folder name:'))
+ self.videoFolderName = QLineEdit()
+ self.videoFolderName.setText(self.prefs.video_backup_identifier)
+ self.videoFolderName.editingFinished.connect(self.videoFolderIdentifierChanged)
+
+ self.autoBackupExampleBox = QGroupBox(_('Example:'))
+ self.autoBackupExample = QLabel()
+
+ autoBackupExampleBoxLayout = QHBoxLayout()
+ autoBackupExampleBoxLayout.addWidget(self.autoBackupExample)
+
+ self.autoBackupExampleBox.setLayout(autoBackupExampleBoxLayout)
+
+ valid_mounts = ValidMounts(onlyExternalMounts=True)
+
+ self.manualLocationExplanation = QLabel(
+ _('If you disable automatic detection, choose the exact backup locations.')
+ )
+ self.manualLocationExplanation.setWordWrap(True)
+ self.manualLocationExplanation.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum)
+ # Translators: the word 'location' is optional in your translation. The left
+ # side of the folder chooser combo box will always line up with the left side of the
+ # the text entry boxes where the user can enter the photo folder name and the video
+ # folder name. See http://damonlynch.net/rapid/documentation/thumbnails/backup.png
+ self.photoLocationLabel = QLabel(_('Photo backup location:'))
+ self.photoLocationLabel.setWordWrap(True)
+ self.photoLocation = FolderCombo(
+ self,
+ prefs=self.prefs,
+ file_type=FileType.photo,
+ file_chooser_title=_('Select Photo Backup Location'),
+ special_dirs=(StandardFileLocations.pictures,),
+ valid_mounts=valid_mounts
+ )
+ self.photoLocation.setPath(self.prefs.backup_photo_location)
+ self.photoLocation.pathChosen.connect(self.photoPathChosen)
+
+ # Translators: the word 'location' is optional in your translation. The left
+ # side of the folder chooser combo box will always line up with the left side of the
+ # the text entry boxes where the user can enter the photo folder name and the video
+ # folder name. See http://damonlynch.net/rapid/documentation/thumbnails/backup.png
+ self.videoLocationLabel = QLabel(_('Video backup location:'))
+ self.videoLocationLabel.setWordWrap(True)
+ self.videoLocation = FolderCombo(
+ self,
+ prefs=self.prefs,
+ file_type=FileType.video,
+ file_chooser_title=_('Select Video Backup Location'),
+ special_dirs=(StandardFileLocations.videos, ),
+ valid_mounts=valid_mounts
+ )
+ self.videoLocation.setPath(self.prefs.backup_video_location)
+ self.videoLocation.pathChosen.connect(self.videoPathChosen)
+
+ backupLayout.addWidget(self.backupExplanation, 0, 0, 1, 4)
+ backupLayout.addWidget(self.backup, 1, 0, 1, 4)
+ backupLayout.addWidget(self.autoBackup, 2, 1, 1, 3)
+ backupLayout.addWidget(self.folderExplanation, 3, 2, 1, 2)
+ backupLayout.addWidget(self.photoFolderNameLabel, 4, 2, 1, 1)
+ backupLayout.addWidget(self.photoFolderName, 4, 3, 1, 1)
+ backupLayout.addWidget(self.videoFolderNameLabel, 5, 2, 1, 1)
+ backupLayout.addWidget(self.videoFolderName, 5, 3, 1, 1)
+ backupLayout.addWidget(self.autoBackupExampleBox, 6, 2, 1, 2)
+ backupLayout.addWidget(self.manualLocationExplanation, 7, 1, 1, 3, Qt.AlignBottom)
+ backupLayout.addWidget(self.photoLocationLabel, 8, 1, 1, 2)
+ backupLayout.addWidget(self.photoLocation, 8, 3, 1, 1)
+ backupLayout.addWidget(self.videoLocationLabel, 9, 1, 1, 2)
+ backupLayout.addWidget(self.videoLocation, 9, 3, 1, 1)
+
+ backupLayout.setColumnMinimumWidth(0, checkbox_width)
+ backupLayout.setColumnMinimumWidth(1, checkbox_width)
+
+ backupLayout.setRowMinimumHeight(7, checkbox_width * 2)
+
+ layout.addStretch()
+
+ self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
+ self.setBackupButtonHighlight()
+
+ # Group controls to enable / disable sets of them
+ self._backup_controls_type = (self.autoBackup, )
+ self._backup_controls_auto = (
+ self.folderExplanation, self.photoFolderNameLabel, self.photoFolderName,
+ self.videoFolderNameLabel, self.videoFolderName, self.autoBackupExampleBox
+ )
+ self._backup_controls_manual = (
+ self.manualLocationExplanation, self.photoLocationLabel, self.photoLocation,
+ self.videoLocationLabel, self.videoLocation,
+ )
+ self.updateExample()
+ self.enableControlsByBackupType()
+
+ @pyqtSlot(int)
+ def backupChanged(self, state: int) -> None:
+ backup = state == Qt.Checked
+ logging.info("Setting backup while downloading to %s", backup)
+ self.prefs.backup_files = backup
+ self.setBackupButtonHighlight()
+ self.enableControlsByBackupType()
+ self.rapidApp.resetupBackupDevices()
+
+ @pyqtSlot(int)
+ def autoBackupChanged(self, state: int) -> None:
+ autoBackup = state == Qt.Checked
+ logging.info("Setting automatically detect backup devices to %s", autoBackup)
+ self.prefs.backup_device_autodetection = autoBackup
+ self.setBackupButtonHighlight()
+ self.enableControlsByBackupType()
+ self.rapidApp.resetupBackupDevices()
+
+ @pyqtSlot(str)
+ def photoPathChosen(self, path: str) -> None:
+ logging.info("Setting backup photo location to %s", path)
+ self.prefs.backup_photo_location = path
+ self.setBackupButtonHighlight()
+ self.rapidApp.resetupBackupDevices()
+
+ @pyqtSlot(str)
+ def videoPathChosen(self, path: str) -> None:
+ logging.info("Setting backup video location to %s", path)
+ self.prefs.backup_video_location = path
+ self.setBackupButtonHighlight()
+ self.rapidApp.resetupBackupDevices()
+
+ @pyqtSlot()
+ def photoFolderIdentifierChanged(self) -> None:
+ name = self.photoFolderName.text()
+ logging.info("Setting backup photo folder name to %s", name)
+ self.prefs.photo_backup_identifier = name
+ self.setBackupButtonHighlight()
+ self.rapidApp.resetupBackupDevices()
+
+ @pyqtSlot()
+ def videoFolderIdentifierChanged(self) -> None:
+ name = self.videoFolderName.text()
+ logging.info("Setting backup video folder name to %s", name)
+ self.prefs.video_backup_identifier = name
+ self.setBackupButtonHighlight()
+ self.rapidApp.resetupBackupDevices()
+
+ def updateExample(self) -> None:
+ """
+ Update the example paths in the backup panel
+ """
+
+ if self.autoBackup.isChecked() and hasattr(self.rapidApp, 'backup_devices') and len(
+ self.rapidApp.backup_devices):
+ drives = self.rapidApp.backup_devices.sample_device_paths()
+ else:
+ # Translators: this value is used as an example device when automatic backup device
+ # detection is enabled. You should translate this.
+ drive1 = os.path.join(self.media_dir, _("drive1"))
+ # Translators: this value is used as an example device when automatic backup device
+ # detection is enabled. You should translate this.
+ drive2 = os.path.join(self.media_dir, _("drive2"))
+ drives = (
+ os.path.join(path, identifier) for path, identifier in (
+ (drive1, self.prefs.photo_backup_identifier),
+ (drive2, self.prefs.photo_backup_identifier),
+ (drive2, self.prefs.video_backup_identifier)
+ )
+ )
+ paths = '\n'.join(drives)
+ self.autoBackupExample.setText(paths)
+
+ def setBackupButtonHighlight(self) -> None:
+ """
+ Indicate error status in GUI by highlighting Backup button.
+
+ Do so only if doing manual backups and there is a problem with one of the paths
+ """
+
+ self.rapidApp.backupButton.setHighlighted(
+ self.prefs.backup_files and not self.prefs.backup_device_autodetection and (
+ self.photoLocation.invalid_path or self.videoLocation.invalid_path))
+
+ def enableControlsByBackupType(self) -> None:
+ """
+ Enable or disable backup controls depending on what the user
+ has enabled.
+ """
+
+ backupsEnabled = self.backup.isChecked()
+ autoEnabled = backupsEnabled and self.autoBackup.isChecked()
+ manualEnabled = not autoEnabled and backupsEnabled
+
+ for widget in self._backup_controls_type:
+ widget.setEnabled(backupsEnabled)
+ for widget in self._backup_controls_manual:
+ widget.setEnabled(manualEnabled)
+ for widget in self._backup_controls_auto:
+ widget.setEnabled(autoEnabled)
+
+ def updateLocationCombos(self) -> None:
+ """
+ Update backup locatation comboboxes in case directory status has changed.
+ """
+ for combo in self.photoLocation, self.videoLocation:
+ combo.refreshFolderList()
+
+
+class BackupPanel(QScrollArea):
+ """
+ Backup preferences widget, for photos and video backups while
+ downloading.
+ """
+
+ def __init__(self, parent) -> None:
+ super().__init__(parent)
+
+ assert parent is not None
+ self.rapidApp = parent
+ self.prefs = self.rapidApp.prefs # type: Preferences
+
+ self.backupDevices = BackupDeviceModel(parent=self)
+
+ self.setFrameShape(QFrame.NoFrame)
+
+ self.backupStoragePanel = QPanelView(
+ label=_('Projected Backup Storage Use'),
+ headerColor=QColor(ThumbnailBackgroundName),
+ headerFontColor=QColor(Qt.white)
+ )
+
+ self.backupOptionsPanel = QPanelView(
+ label=_('Backup Options'),
+ headerColor=QColor(ThumbnailBackgroundName),
+ headerFontColor=QColor(Qt.white)
+ )
+
+ self.backupDevicesView = BackupDeviceView(rapidApp=self.rapidApp, parent=self)
+ self.backupStoragePanel.addWidget(self.backupDevicesView)
+ self.backupDevicesView.setModel(self.backupDevices)
+ self.backupDevicesView.setItemDelegate(BackupDeviceDelegate(rapidApp=self.rapidApp))
+ self.backupDevicesView.setSizePolicy(
+ QSizePolicy.MinimumExpanding, QSizePolicy.Fixed
+ )
+ self.backupOptionsPanel.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding)
+
+ self.backupOptions = BackupOptionsWidget(
+ prefs=self.prefs, parent=self, rapidApp=self.rapidApp
+ )
+ self.backupOptionsPanel.addWidget(self.backupOptions)
+
+ widget = QWidget()
+ layout = QVBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ widget.setLayout(layout)
+ layout.addWidget(self.backupStoragePanel)
+ layout.addWidget(self.backupOptionsPanel)
+ # layout.addStretch()
+ self.setWidget(widget)
+ self.setWidgetResizable(True)
+ self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
+
+ def updateExample(self) -> None:
+ """
+ Update the example paths in the backup panel
+ """
+
+ self.backupOptions.updateExample()
+
+ def updateLocationCombos(self) -> None:
+ """
+ Update backup locatation comboboxes in case directory status has changed.
+ """
+
+ self.backupOptions.updateLocationCombos()
+
+ def addBackupVolume(self, mount_details: BackupVolumeDetails) -> None:
+ self.backupDevices.addBackupVolume(mount_details=mount_details)
+ self.backupDevicesView.updateGeometry()
+
+ def removeBackupVolume(self, path: str) -> None:
+ self.backupDevices.removeBackupVolume(path=path)
+ self.backupDevicesView.updateGeometry()
+
+ def resetBackupDisplay(self) -> None:
+ self.backupDevices.reset()
+ self.backupDevicesView.updateGeometry()
+
+ def setupBackupDisplay(self) -> None:
+ """
+ Sets up the backup view list regardless of whether backups
+ are manual specified by the user, or auto-detection is on
+ """
+
+ if not self.prefs.backup_files:
+ logging.debug("No backups configured: no backup destinations to display")
+ return
+
+ backup_devices = self.rapidApp.backup_devices # type: BackupDeviceCollection
+ if self.prefs.backup_device_autodetection:
+ for path in backup_devices:
+ self.backupDevices.addBackupVolume(
+ mount_details=backup_devices.get_backup_volume_details(path=path))
+ else:
+ # manually specified backup paths
+ try:
+ mounts = backup_devices.get_manual_mounts()
+ if mounts is None:
+ return
+
+ self.backupDevices.addBackupVolume(mount_details=mounts[0])
+ if len(mounts) > 1:
+ self.backupDevices.addBackupVolume(mount_details=mounts[1])
+ except Exception:
+ logging.exception(
+ 'An unexpected error occurred when adding backup destinations. Exception:'
+ )
+ self.backupDevicesView.updateGeometry()
+
+ def setDownloadAttributes(self, marked: FileTypeCounter,
+ photos_size: int,
+ videos_size: int,
+ merge: bool) -> None:
+ """
+ Set the attributes used to generate the visual display of the
+ files marked to be downloaded
+
+ :param marked: number and type of files marked for download
+ :param photos_size: size in bytes of photos marked for download
+ :param videos_size: size in bytes of videos marked for download
+ :param merge: whether to replace or add to the current values
+ """
+
+ self.backupDevices.setDownloadAttributes(
+ marked=marked, photos_size=photos_size, videos_size=videos_size, merge=merge
+ )
+
+ def sufficientSpaceAvailable(self) -> bool:
+ """
+ Check to see that there is sufficient space with which to perform a download.
+
+ :return: True or False value if sufficient space. Will always return True if
+ backups are disabled or there are no backup devices.
+ """
+ if self.prefs.backup_files:
+ return self.backupDevices.sufficientSpaceAvailable()
+ else:
+ return True
+
+ def setDownloadingTo(self, downloading_to: DefaultDict[int, Set[FileType]]) -> None:
+ self.backupDevices.downloading_to = downloading_to
+