This file is indexed.

/usr/lib/python3/dist-packages/raphodo/backuppanel.py is in rapid-photo-downloader 0.9.9-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
# 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
            elif 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