File indexing completed on 2024-04-14 05:39:30

0001 // SPDX-FileCopyrightText: 2020 Simon Persson <simon.persson@mykolab.com>
0002 //
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "driveselection.h"
0006 #include "driveselectiondelegate.h"
0007 #include "backupplan.h"
0008 
0009 #include <QItemSelectionModel>
0010 #include <QList>
0011 #include <QPainter>
0012 #include <QStandardItemModel>
0013 #include <QTimer>
0014 
0015 #include <KConfigDialogManager>
0016 #include <QStorageInfo>
0017 #include <KLocalizedString>
0018 
0019 #include <Solid/Device>
0020 #include <Solid/DeviceNotifier>
0021 #include <Solid/StorageAccess>
0022 #include <Solid/StorageDrive>
0023 #include <Solid/StorageVolume>
0024 
0025 bool deviceLessThan(const Solid::Device &a, const Solid::Device &b) {
0026     return a.udi() < b.udi();
0027 }
0028 
0029 DriveSelection::DriveSelection(BackupPlan *pBackupPlan, QWidget *parent)
0030    : QListView(parent), mBackupPlan(pBackupPlan), mSelectedAndAccessible(false), mSyncedBackupType(false)
0031 {
0032     mDrivesModel = new QStandardItemModel(this);
0033     setModel(mDrivesModel);
0034     setItemDelegate(new DriveSelectionDelegate(this));
0035     setSelectionMode(QAbstractItemView::SingleSelection);
0036     setWordWrap(true);
0037 
0038     if(!mBackupPlan->mExternalUUID.isEmpty()) {
0039         auto *lItem = new QStandardItem();
0040         lItem->setEditable(false);
0041         lItem->setData(QString(), DriveSelection::UDI);
0042         lItem->setData(mBackupPlan->mExternalUUID, DriveSelection::UUID);
0043         lItem->setData(0, DriveSelection::UsedSpace);
0044         lItem->setData(mBackupPlan->mExternalPartitionNumber, DriveSelection::PartitionNumber);
0045         lItem->setData(mBackupPlan->mExternalPartitionsOnDrive, DriveSelection::PartitionsOnDrive);
0046         lItem->setData(mBackupPlan->mExternalDeviceDescription, DriveSelection::DeviceDescription);
0047         lItem->setData(mBackupPlan->mExternalVolumeCapacity, DriveSelection::TotalSpace);
0048         lItem->setData(mBackupPlan->mExternalVolumeLabel, DriveSelection::Label);
0049         mDrivesModel->appendRow(lItem);
0050     }
0051 
0052     QList<Solid::Device> lDeviceList = Solid::Device::listFromType(Solid::DeviceInterface::StorageDrive);
0053     foreach (const Solid::Device &lDevice, lDeviceList) {
0054         deviceAdded(lDevice.udi());
0055     }
0056     connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceAdded(QString)), SLOT(deviceAdded(QString)));
0057     connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceRemoved(QString)), SLOT(deviceRemoved(QString)));
0058     connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
0059             this, SLOT(updateSelection(QItemSelection,QItemSelection)));
0060 }
0061 
0062 QString DriveSelection::mountPathOfSelectedDrive() const {
0063     if(mSelectedAndAccessible) {
0064         QStandardItem *lItem;
0065         findItem(DriveSelection::UUID, mSelectedUuid, &lItem);
0066         if(lItem != nullptr) {
0067             Solid::Device lDevice(lItem->data(DriveSelection::UDI).toString());
0068             auto *lAccess = lDevice.as<Solid::StorageAccess>();
0069             if(lAccess) {
0070                 return lAccess->filePath();
0071             }
0072         }
0073     }
0074     return QString();
0075 }
0076 
0077 void DriveSelection::deviceAdded(const QString &pUdi) {
0078     Solid::Device lDevice(pUdi);
0079     if(!lDevice.is<Solid::StorageDrive>()) {
0080         return;
0081     }
0082     auto *lDrive = lDevice.as<Solid::StorageDrive>();
0083     if(!lDrive->isHotpluggable() && !lDrive->isRemovable()) {
0084         return;
0085     }
0086     if(mDrivesToAdd.contains(pUdi)) {
0087         return;
0088     }
0089     mDrivesToAdd.append(pUdi);
0090     QTimer::singleShot(2000, this, SLOT(delayedDeviceAdded()));
0091 }
0092 
0093 void DriveSelection::delayedDeviceAdded() {
0094     if(mDrivesToAdd.isEmpty()) {
0095         return;
0096     }
0097     Solid::Device lParentDevice(mDrivesToAdd.takeFirst());
0098     QList<Solid::Device> lDeviceList = Solid::Device::listFromType(Solid::DeviceInterface::StorageVolume,
0099                                                                    lParentDevice.udi());
0100     // check for when there is no partitioning scheme, then the drive is also the storage volume
0101     if(lParentDevice.is<Solid::StorageVolume>()) {
0102         lDeviceList.append(lParentDevice);
0103     }
0104 
0105     // filter out some volumes that should not be visible
0106     QList<Solid::Device> lVolumeDeviceList;
0107     foreach(Solid::Device lVolumeDevice, lDeviceList) {
0108         auto *lVolume = lVolumeDevice.as<Solid::StorageVolume>();
0109         if(lVolume && !lVolume->isIgnored() && (
0110                  lVolume->usage() == Solid::StorageVolume::FileSystem ||
0111                  lVolume->usage() == Solid::StorageVolume::Encrypted)) {
0112             lVolumeDeviceList.append(lVolumeDevice);
0113         }
0114     }
0115 
0116     // simplest attempt at getting the same partition numbering every time a device is plugged in
0117     std::sort(lVolumeDeviceList.begin(), lVolumeDeviceList.end(), deviceLessThan);
0118 
0119     int lPartitionNumber = 1;
0120     foreach(Solid::Device lVolumeDevice, lVolumeDeviceList) {
0121         auto *lVolume = lVolumeDevice.as<Solid::StorageVolume>();
0122         QString lUuid = lVolume->uuid();
0123         if(lUuid.isEmpty()) { //seems to happen for vfat partitions
0124             lUuid += lParentDevice.description();
0125             lUuid += QStringLiteral("|");
0126             lUuid += lVolume->label();
0127         }
0128         QStandardItem *lItem;
0129         bool lNeedsToBeAdded = false;
0130         findItem(DriveSelection::UUID, lUuid, &lItem);
0131         if(lItem == nullptr) {
0132             lItem = new QStandardItem();
0133             lItem->setEditable(false);
0134             lItem->setData(lUuid, DriveSelection::UUID);
0135             lItem->setData(0, DriveSelection::TotalSpace);
0136             lItem->setData(0, DriveSelection::UsedSpace);
0137             lNeedsToBeAdded = true;
0138         }
0139         lItem->setData(lParentDevice.description(), DriveSelection::DeviceDescription);
0140         lItem->setData(lVolume->label(), DriveSelection::Label);
0141         lItem->setData(lVolumeDevice.udi(), DriveSelection::UDI);
0142         lItem->setData(lPartitionNumber, DriveSelection::PartitionNumber);
0143         lItem->setData(lVolumeDeviceList.count(), DriveSelection::PartitionsOnDrive);
0144         lItem->setData(lVolume->fsType(), DriveSelection::FileSystem);
0145         lItem->setData(mSyncedBackupType && (lVolume->fsType() == QStringLiteral("vfat") ||
0146                                              lVolume->fsType() == QStringLiteral("ntfs")),
0147                        DriveSelection::PermissionLossWarning);
0148         lItem->setData(mSyncedBackupType && lVolume->fsType() == QStringLiteral("vfat"),
0149                        DriveSelection::SymlinkLossWarning);
0150 
0151         auto *lAccess = lVolumeDevice.as<Solid::StorageAccess>();
0152         connect(lAccess, SIGNAL(accessibilityChanged(bool,QString)), SLOT(accessabilityChanged(bool,QString)));
0153         if(lAccess->isAccessible()) {
0154             QStorageInfo storageInfo(lAccess->filePath());
0155             if(storageInfo.isValid()) {
0156                 lItem->setData(storageInfo.bytesTotal(), DriveSelection::TotalSpace);
0157                 lItem->setData(storageInfo.bytesTotal() - storageInfo.bytesFree(), DriveSelection::UsedSpace);
0158             }
0159             if(lUuid == mSelectedUuid) {
0160                 // Selected volume was just added, could not have been accessible before.
0161                 mSelectedAndAccessible = true;
0162                 emit selectedDriveIsAccessibleChanged(true);
0163             }
0164         } else if(lVolume->usage() != Solid::StorageVolume::Encrypted) {
0165             // Don't bother the user with password prompt just for sake of storage volume space info
0166             lAccess->setup();
0167         }
0168         if(lNeedsToBeAdded) {
0169             mDrivesModel->appendRow(lItem);
0170             if(mDrivesModel->rowCount() == 1) {
0171                 selectionModel()->select(mDrivesModel->index(0, 0), QItemSelectionModel::ClearAndSelect);
0172             }
0173         }
0174         lPartitionNumber++;
0175     }
0176 }
0177 
0178 void DriveSelection::deviceRemoved(const QString &pUdi) {
0179     QStandardItem *lItem;
0180     int lRow = findItem(DriveSelection::UDI, pUdi, &lItem);
0181     if(lRow >= 0) {
0182         QString lUuid = lItem->data(DriveSelection::UUID).toString();
0183         if(lUuid == mBackupPlan->mExternalUUID) {
0184             // let the selected and saved item stay in the list
0185             // just clear the UDI so that it will be shown as disconnected.
0186             lItem->setData(QString(), DriveSelection::UDI);
0187         } else {
0188             mDrivesModel->removeRow(lRow);
0189         }
0190         if(lUuid == mSelectedUuid && mSelectedAndAccessible) {
0191             mSelectedAndAccessible = false;
0192             emit selectedDriveIsAccessibleChanged(false);
0193         }
0194     }
0195 }
0196 
0197 void DriveSelection::accessabilityChanged(bool pAccessible, const QString &pUdi) {
0198     QStandardItem *lItem;
0199     findItem(DriveSelection::UDI, pUdi, &lItem);
0200     if(lItem != nullptr) {
0201         if(pAccessible) {
0202             Solid::Device lDevice(pUdi);
0203             auto *lAccess = lDevice.as<Solid::StorageAccess>();
0204             if(lAccess) {
0205                 QStorageInfo storageInfo(lAccess->filePath());
0206                 if(storageInfo.isValid()) {
0207                     lItem->setData(storageInfo.bytesTotal(), DriveSelection::TotalSpace);
0208                     lItem->setData(storageInfo.bytesTotal() - storageInfo.bytesFree(), DriveSelection::UsedSpace);
0209                 }
0210             }
0211         }
0212         bool lSelectedAndAccessible = (lItem->data(DriveSelection::UUID).toString() == mSelectedUuid && pAccessible);
0213         if(lSelectedAndAccessible != mSelectedAndAccessible) {
0214             mSelectedAndAccessible = lSelectedAndAccessible;
0215             emit selectedDriveIsAccessibleChanged(lSelectedAndAccessible);
0216         }
0217     }
0218 }
0219 
0220 void DriveSelection::updateSelection(const QItemSelection &pSelected, const QItemSelection &pDeselected) {
0221     Q_UNUSED(pDeselected)
0222     if(!pSelected.indexes().isEmpty()) {
0223         QModelIndex lIndex = pSelected.indexes().first();
0224         if(mSelectedUuid.isEmpty()) {
0225             emit driveIsSelectedChanged(true);
0226         }
0227         mSelectedUuid = lIndex.data(DriveSelection::UUID).toString();
0228         emit selectedDriveChanged(mSelectedUuid);
0229         // check if the newly selected volume is accessible, compare to previous selection
0230         bool lIsAccessible = false;
0231         QString lUdiOfSelected = lIndex.data(DriveSelection::UDI).toString();
0232         if(!lUdiOfSelected.isEmpty()) {
0233             Solid::Device lDevice(lUdiOfSelected);
0234             auto *lAccess = lDevice.as<Solid::StorageAccess>();
0235             if(lAccess != nullptr) {
0236                 lIsAccessible = lAccess->isAccessible();
0237             }
0238         }
0239         if(mSelectedAndAccessible != lIsAccessible) {
0240             mSelectedAndAccessible = lIsAccessible;
0241             emit selectedDriveIsAccessibleChanged(mSelectedAndAccessible);
0242         }
0243     } else {
0244         mSelectedUuid.clear();
0245         emit selectedDriveChanged(mSelectedUuid);
0246         emit driveIsSelectedChanged(false);
0247         mSelectedAndAccessible = false;
0248         emit selectedDriveIsAccessibleChanged(false);
0249     }
0250 }
0251 
0252 void DriveSelection::paintEvent(QPaintEvent *pPaintEvent) {
0253     QListView::paintEvent(pPaintEvent);
0254     if(mDrivesModel->rowCount() == 0) {
0255         QPainter lPainter(viewport());
0256         style()->drawItemText(&lPainter, rect(), Qt::AlignCenter, palette(), false,
0257                               xi18nc("@label Only shown if no drives are detected", "Plug in the external "
0258                                     "storage you wish to use, then select it in this list."), QPalette::Text);
0259     }
0260 }
0261 
0262 void DriveSelection::setSelectedDrive(const QString &pUuid) {
0263     if(pUuid == mSelectedUuid) {
0264         return;
0265     }
0266     if(pUuid.isEmpty()) {
0267         clearSelection();
0268     } else {
0269         QStandardItem *lItem;
0270         findItem(DriveSelection::UUID, pUuid, &lItem);
0271         if(lItem != nullptr) {
0272             setCurrentIndex(mDrivesModel->indexFromItem(lItem));
0273         }
0274     }
0275 }
0276 
0277 void DriveSelection::saveExtraData() {
0278     QStandardItem *lItem;
0279     findItem(DriveSelection::UUID, mSelectedUuid, &lItem);
0280     if(lItem != nullptr) {
0281         mBackupPlan->mExternalDeviceDescription = lItem->data(DriveSelection::DeviceDescription).toString();
0282         mBackupPlan->mExternalPartitionNumber = lItem->data(DriveSelection::PartitionNumber).toInt();
0283         mBackupPlan->mExternalPartitionsOnDrive = lItem->data(DriveSelection::PartitionsOnDrive).toInt();
0284         mBackupPlan->mExternalVolumeCapacity = lItem->data(DriveSelection::TotalSpace).toULongLong();
0285         mBackupPlan->mExternalVolumeLabel = lItem->data(DriveSelection::Label).toString();
0286     }
0287 }
0288 
0289 void DriveSelection::updateSyncWarning(bool pSyncBackupSelected) {
0290     mSyncedBackupType = pSyncBackupSelected;
0291     for(int i = 0; i < mDrivesModel->rowCount(); ++i) {
0292         QString lFsType = mDrivesModel->item(i)->data(DriveSelection::FileSystem).toString();
0293         mDrivesModel->item(i)->setData(mSyncedBackupType && (lFsType == QStringLiteral("vfat") ||
0294                                                              lFsType == QStringLiteral("ntfs")),
0295                                        DriveSelection::PermissionLossWarning);
0296         mDrivesModel->item(i)->setData(mSyncedBackupType && lFsType == QStringLiteral("vfat"),
0297                                        DriveSelection::SymlinkLossWarning);
0298     }
0299 }
0300 
0301 int DriveSelection::findItem(const DriveSelection::DataType pField, const QString &pSearchString,
0302                              QStandardItem **pReturnedItem) const {
0303     for(int lRow = 0; lRow < mDrivesModel->rowCount(); ++lRow) {
0304         QStandardItem *lItem = mDrivesModel->item(lRow);
0305         if(lItem->data(pField).toString() == pSearchString) {
0306             if(pReturnedItem != nullptr) {
0307                 *pReturnedItem = lItem;
0308             }
0309             return lRow;
0310         }
0311     }
0312     if(pReturnedItem != nullptr) {
0313         *pReturnedItem = nullptr;
0314     }
0315     return -1;
0316 }