1 /*
2  * Copyright (C) by Klaas Freitag <freitag@kde.org>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12  * for more details.
13  */
14 
15 #include "folderstatusmodel.h"
16 #include "folderman.h"
17 #include "accountstate.h"
18 #include "common/asserts.h"
19 #include <theme.h>
20 #include <account.h>
21 #include "folderstatusdelegate.h"
22 
23 #include <QFileIconProvider>
24 #include <QVarLengthArray>
25 #include <set>
26 
27 Q_DECLARE_METATYPE(QPersistentModelIndex)
28 
29 namespace OCC {
30 
31 Q_LOGGING_CATEGORY(lcFolderStatus, "gui.folder.model", QtInfoMsg)
32 
33 static const char propertyParentIndexC[] = "oc_parentIndex";
34 static const char propertyPermissionMap[] = "oc_permissionMap";
35 
removeTrailingSlash(const QString & s)36 static QString removeTrailingSlash(const QString &s)
37 {
38     if (s.endsWith('/')) {
39         return s.left(s.size() - 1);
40     }
41     return s;
42 }
43 
FolderStatusModel(QObject * parent)44 FolderStatusModel::FolderStatusModel(QObject *parent)
45     : QAbstractItemModel(parent)
46     , _accountState(nullptr)
47     , _dirty(false)
48 {
49 }
50 
~FolderStatusModel()51 FolderStatusModel::~FolderStatusModel()
52 {
53 }
54 
sortByFolderHeader(const FolderStatusModel::SubFolderInfo & lhs,const FolderStatusModel::SubFolderInfo & rhs)55 static bool sortByFolderHeader(const FolderStatusModel::SubFolderInfo &lhs, const FolderStatusModel::SubFolderInfo &rhs)
56 {
57     return QString::compare(lhs._folder->shortGuiRemotePathOrAppName(),
58                rhs._folder->shortGuiRemotePathOrAppName(),
59                Qt::CaseInsensitive)
60         < 0;
61 }
62 
setAccountState(const AccountState * accountState)63 void FolderStatusModel::setAccountState(const AccountState *accountState)
64 {
65     beginResetModel();
66     _dirty = false;
67     _folders.clear();
68     _accountState = accountState;
69 
70     connect(FolderMan::instance(), &FolderMan::folderSyncStateChange,
71         this, &FolderStatusModel::slotFolderSyncStateChange, Qt::UniqueConnection);
72     connect(FolderMan::instance(), &FolderMan::scheduleQueueChanged,
73         this, &FolderStatusModel::slotFolderScheduleQueueChanged, Qt::UniqueConnection);
74 
75     auto folders = FolderMan::instance()->map();
76     foreach (auto f, folders) {
77         if (!accountState)
78             break;
79         if (f->accountState() != accountState)
80             continue;
81         SubFolderInfo info;
82         info._name = f->alias();
83         info._path = "/";
84         info._folder = f;
85         info._checked = Qt::PartiallyChecked;
86         _folders << info;
87 
88         connect(f, &Folder::progressInfo, this, &FolderStatusModel::slotSetProgress, Qt::UniqueConnection);
89         connect(f, &Folder::newBigFolderDiscovered, this, &FolderStatusModel::slotNewBigFolder, Qt::UniqueConnection);
90     }
91 
92     // Sort by header text
93     std::sort(_folders.begin(), _folders.end(), sortByFolderHeader);
94 
95     // Set the root _pathIdx after the sorting
96     for (int i = 0; i < _folders.size(); ++i) {
97         _folders[i]._pathIdx << i;
98     }
99 
100     endResetModel();
101     emit dirtyChanged();
102 }
103 
104 
flags(const QModelIndex & index) const105 Qt::ItemFlags FolderStatusModel::flags(const QModelIndex &index) const
106 {
107     if (!_accountState) {
108         return nullptr;
109     }
110     switch (classify(index)) {
111     case AddButton: {
112         Qt::ItemFlags ret;
113         ret = Qt::ItemNeverHasChildren;
114         if (!_accountState->isConnected()) {
115             return ret;
116         }
117         return Qt::ItemIsEnabled | ret;
118     }
119     case FetchLabel:
120         return Qt::ItemIsEnabled | Qt::ItemNeverHasChildren;
121     case RootFolder:
122         return Qt::ItemIsEnabled;
123     case SubFolder:
124         return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable;
125     }
126     return nullptr;
127 }
128 
data(const QModelIndex & index,int role) const129 QVariant FolderStatusModel::data(const QModelIndex &index, int role) const
130 {
131     if (!index.isValid())
132         return QVariant();
133 
134     if (role == Qt::EditRole)
135         return QVariant();
136 
137     switch (classify(index)) {
138     case AddButton: {
139         if (role == FolderStatusDelegate::AddButton) {
140             return QVariant(true);
141         } else if (role == Qt::ToolTipRole) {
142             if (!_accountState->isConnected()) {
143                 return tr("You need to be connected to add a folder");
144             }
145             return tr("Click this button to add a folder to synchronize.");
146         }
147         return QVariant();
148     }
149     case SubFolder: {
150         const auto &x = static_cast<SubFolderInfo *>(index.internalPointer())->_subs.at(index.row());
151         switch (role) {
152         case Qt::DisplayRole:
153             //: Example text: "File.txt (23KB)"
154             return x._size < 0 ? x._name : tr("%1 (%2)").arg(x._name, Utility::octetsToString(x._size));
155         case Qt::ToolTipRole:
156             return QString(QLatin1String("<qt>") + Utility::escape(x._size < 0 ? x._name : tr("%1 (%2)").arg(x._name, Utility::octetsToString(x._size))) + QLatin1String("</qt>"));
157         case Qt::CheckStateRole:
158             return x._checked;
159         case Qt::DecorationRole:
160             return QFileIconProvider().icon(x._isExternal ? QFileIconProvider::Network : QFileIconProvider::Folder);
161         case Qt::ForegroundRole:
162             if (x._isUndecided) {
163                 return QColor(Qt::red);
164             }
165             break;
166         case FolderStatusDelegate::FolderPathRole: {
167             auto f = x._folder;
168             if (!f)
169                 return QVariant();
170             return QVariant(f->path() + x._path);
171         }
172         }
173     }
174         return QVariant();
175     case FetchLabel: {
176         const auto x = static_cast<SubFolderInfo *>(index.internalPointer());
177         switch (role) {
178         case Qt::DisplayRole:
179             if (x->_hasError) {
180                 return QVariant(tr("Error while loading the list of folders from the server.")
181                     + QString("\n") + x->_lastErrorString);
182             } else {
183                 return tr("Fetching folder list from server...");
184             }
185             break;
186         default:
187             return QVariant();
188         }
189     }
190     case RootFolder:
191         break;
192     }
193 
194     const SubFolderInfo &folderInfo = _folders.at(index.row());
195     auto f = folderInfo._folder;
196     if (!f)
197         return QVariant();
198 
199     const SubFolderInfo::Progress &progress = folderInfo._progress;
200     const bool accountConnected = _accountState->isConnected();
201 
202     switch (role) {
203     case FolderStatusDelegate::FolderPathRole:
204         return f->shortGuiLocalPath();
205     case FolderStatusDelegate::FolderSecondPathRole:
206         return f->remotePath();
207     case FolderStatusDelegate::FolderConflictMsg:
208         return (f->syncResult().hasUnresolvedConflicts())
209             ? QStringList(tr("There are unresolved conflicts. Click for details."))
210             : QStringList();
211     case FolderStatusDelegate::FolderErrorMsg:
212         return f->syncResult().errorStrings();
213     case FolderStatusDelegate::FolderInfoMsg:
214         return f->virtualFilesEnabled() && f->vfs().mode() != Vfs::Mode::WindowsCfApi
215             ? QStringList(tr("Virtual file support is enabled."))
216             : QStringList();
217     case FolderStatusDelegate::SyncRunning:
218         return f->syncResult().status() == SyncResult::SyncRunning;
219     case FolderStatusDelegate::HeaderRole:
220         return f->shortGuiRemotePathOrAppName();
221     case FolderStatusDelegate::FolderAliasRole:
222         return f->alias();
223     case FolderStatusDelegate::FolderSyncPaused:
224         return f->syncPaused();
225     case FolderStatusDelegate::FolderAccountConnected:
226         return accountConnected;
227     case Qt::ToolTipRole: {
228         QString toolTip;
229         if (!progress.isNull()) {
230             return progress._progressString;
231         }
232         if (accountConnected)
233             toolTip = Theme::instance()->statusHeaderText(f->syncResult().status());
234         else
235             toolTip = tr("Signed out");
236         toolTip += "\n";
237         toolTip += folderInfo._folder->path();
238         return toolTip;
239     }
240     case FolderStatusDelegate::FolderStatusIconRole:
241         if (accountConnected) {
242             auto theme = Theme::instance();
243             auto status = f->syncResult().status();
244             if (f->syncPaused()) {
245                 return theme->folderDisabledIcon();
246             } else {
247                 if (status == SyncResult::SyncPrepare) {
248                     return theme->syncStateIcon(SyncResult::SyncRunning);
249                 } else if (status == SyncResult::Undefined) {
250                     return theme->syncStateIcon(SyncResult::SyncRunning);
251                 } else {
252                     // The "Problem" *result* just means some files weren't
253                     // synced, so we show "Success" in these cases. But we
254                     // do use the "Problem" *icon* for unresolved conflicts.
255                     if (status == SyncResult::Success || status == SyncResult::Problem) {
256                         if (f->syncResult().hasUnresolvedConflicts()) {
257                             return theme->syncStateIcon(SyncResult::Problem);
258                         } else {
259                             return theme->syncStateIcon(SyncResult::Success);
260                         }
261                     } else {
262                         return theme->syncStateIcon(status);
263                     }
264                 }
265             }
266         } else {
267             return Theme::instance()->folderOfflineIcon();
268         }
269     case FolderStatusDelegate::SyncProgressItemString:
270         return progress._progressString;
271     case FolderStatusDelegate::WarningCount:
272         return progress._warningCount;
273     case FolderStatusDelegate::SyncProgressOverallPercent:
274         return progress._overallPercent;
275     case FolderStatusDelegate::SyncProgressOverallString:
276         return progress._overallSyncString;
277     case FolderStatusDelegate::FolderSyncText:
278         if (f->virtualFilesEnabled()) {
279             return tr("Synchronizing VirtualFiles with local folder");
280         } else {
281             return tr("Synchronizing with local folder");
282         }
283     }
284     return QVariant();
285 }
286 
setData(const QModelIndex & index,const QVariant & value,int role)287 bool FolderStatusModel::setData(const QModelIndex &index, const QVariant &value, int role)
288 {
289     if (role == Qt::CheckStateRole) {
290         auto info = infoForIndex(index);
291         Qt::CheckState checked = static_cast<Qt::CheckState>(value.toInt());
292 
293         if (info && info->_checked != checked) {
294             info->_checked = checked;
295             if (checked == Qt::Checked) {
296                 // If we are checked, check that we may need to check the parent as well if
297                 // all the siblings are also checked
298                 QModelIndex parent = index.parent();
299                 auto parentInfo = infoForIndex(parent);
300                 if (parentInfo && parentInfo->_checked != Qt::Checked) {
301                     bool hasUnchecked = false;
302                     foreach (const auto &sub, parentInfo->_subs) {
303                         if (sub._checked != Qt::Checked) {
304                             hasUnchecked = true;
305                             break;
306                         }
307                     }
308                     if (!hasUnchecked) {
309                         setData(parent, Qt::Checked, Qt::CheckStateRole);
310                     } else if (parentInfo->_checked == Qt::Unchecked) {
311                         setData(parent, Qt::PartiallyChecked, Qt::CheckStateRole);
312                     }
313                 }
314                 // also check all the children
315                 for (int i = 0; i < info->_subs.count(); ++i) {
316                     if (info->_subs.at(i)._checked != Qt::Checked) {
317                         setData(this->index(i, 0, index), Qt::Checked, Qt::CheckStateRole);
318                     }
319                 }
320             }
321 
322             if (checked == Qt::Unchecked) {
323                 QModelIndex parent = index.parent();
324                 auto parentInfo = infoForIndex(parent);
325                 if (parentInfo && parentInfo->_checked == Qt::Checked) {
326                     setData(parent, Qt::PartiallyChecked, Qt::CheckStateRole);
327                 }
328 
329                 // Uncheck all the children
330                 for (int i = 0; i < info->_subs.count(); ++i) {
331                     if (info->_subs.at(i)._checked != Qt::Unchecked) {
332                         setData(this->index(i, 0, index), Qt::Unchecked, Qt::CheckStateRole);
333                     }
334                 }
335             }
336 
337             if (checked == Qt::PartiallyChecked) {
338                 QModelIndex parent = index.parent();
339                 auto parentInfo = infoForIndex(parent);
340                 if (parentInfo && parentInfo->_checked != Qt::PartiallyChecked) {
341                     setData(parent, Qt::PartiallyChecked, Qt::CheckStateRole);
342                 }
343             }
344         }
345         _dirty = true;
346         emit dirtyChanged();
347         emit dataChanged(index, index, QVector<int>() << role);
348         return true;
349     }
350     return QAbstractItemModel::setData(index, value, role);
351 }
352 
353 
columnCount(const QModelIndex &) const354 int FolderStatusModel::columnCount(const QModelIndex &) const
355 {
356     return 1;
357 }
358 
rowCount(const QModelIndex & parent) const359 int FolderStatusModel::rowCount(const QModelIndex &parent) const
360 {
361     if (!parent.isValid()) {
362         if (Theme::instance()->singleSyncFolder() && _folders.count() != 0) {
363             // "Add folder" button not visible in the singleSyncFolder configuration.
364             return _folders.count();
365         }
366         return _folders.count() + 1; // +1 for the "add folder" button
367     }
368     auto info = infoForIndex(parent);
369     if (!info)
370         return 0;
371     if (info->hasLabel())
372         return 1;
373     return info->_subs.count();
374 }
375 
classify(const QModelIndex & index) const376 FolderStatusModel::ItemType FolderStatusModel::classify(const QModelIndex &index) const
377 {
378     if (auto sub = static_cast<SubFolderInfo *>(index.internalPointer())) {
379         if (sub->hasLabel()) {
380             return FetchLabel;
381         } else {
382             return SubFolder;
383         }
384     }
385     if (index.row() < _folders.count()) {
386         return RootFolder;
387     }
388     return AddButton;
389 }
390 
infoForIndex(const QModelIndex & index) const391 FolderStatusModel::SubFolderInfo *FolderStatusModel::infoForIndex(const QModelIndex &index) const
392 {
393     if (!index.isValid())
394         return nullptr;
395     if (auto parentInfo = static_cast<SubFolderInfo *>(index.internalPointer())) {
396         if (parentInfo->hasLabel()) {
397             return nullptr;
398         }
399         if (index.row() >= parentInfo->_subs.size()) {
400             return nullptr;
401         }
402         return &parentInfo->_subs[index.row()];
403     } else {
404         if (index.row() >= _folders.count()) {
405             // AddButton
406             return nullptr;
407         }
408         return const_cast<SubFolderInfo *>(&_folders[index.row()]);
409     }
410 }
411 
indexForPath(Folder * f,const QString & path) const412 QModelIndex FolderStatusModel::indexForPath(Folder *f, const QString &path) const
413 {
414     if (!f) {
415         return QModelIndex();
416     }
417 
418     int slashPos = path.lastIndexOf('/');
419     if (slashPos == -1) {
420         // first level folder
421         for (int i = 0; i < _folders.size(); ++i) {
422             auto &info = _folders.at(i);
423             if (info._folder == f) {
424                 if (path.isEmpty()) { // the folder object
425                     return index(i, 0);
426                 }
427                 for (int j = 0; j < info._subs.size(); ++j) {
428                     const QString subName = info._subs.at(j)._name;
429                     if (subName == path) {
430                         return index(j, 0, index(i));
431                     }
432                 }
433                 return QModelIndex();
434             }
435         }
436         return QModelIndex();
437     }
438 
439     auto parent = indexForPath(f, path.left(slashPos));
440     if (!parent.isValid())
441         return parent;
442 
443     if (slashPos == path.size() - 1) {
444         // The slash is the last part, we found our index
445         return parent;
446     }
447 
448     auto parentInfo = infoForIndex(parent);
449     if (!parentInfo) {
450         return QModelIndex();
451     }
452     for (int i = 0; i < parentInfo->_subs.size(); ++i) {
453         if (parentInfo->_subs.at(i)._name == path.mid(slashPos + 1)) {
454             return index(i, 0, parent);
455         }
456     }
457 
458     return QModelIndex();
459 }
460 
index(int row,int column,const QModelIndex & parent) const461 QModelIndex FolderStatusModel::index(int row, int column, const QModelIndex &parent) const
462 {
463     if (!parent.isValid()) {
464         return createIndex(row, column /*, nullptr*/);
465     }
466     switch (classify(parent)) {
467     case AddButton:
468     case FetchLabel:
469         return QModelIndex();
470     case RootFolder:
471         if (_folders.count() <= parent.row())
472             return QModelIndex(); // should not happen
473         return createIndex(row, column, const_cast<SubFolderInfo *>(&_folders[parent.row()]));
474     case SubFolder: {
475         auto pinfo = static_cast<SubFolderInfo *>(parent.internalPointer());
476         if (pinfo->_subs.count() <= parent.row())
477             return QModelIndex(); // should not happen
478         auto &info = pinfo->_subs[parent.row()];
479         if (!info.hasLabel()
480             && info._subs.count() <= row)
481             return QModelIndex(); // should not happen
482         return createIndex(row, column, &info);
483     }
484     }
485     return QModelIndex();
486 }
487 
parent(const QModelIndex & child) const488 QModelIndex FolderStatusModel::parent(const QModelIndex &child) const
489 {
490     if (!child.isValid()) {
491         return QModelIndex();
492     }
493     switch (classify(child)) {
494     case RootFolder:
495     case AddButton:
496         return QModelIndex();
497     case SubFolder:
498     case FetchLabel:
499         break;
500     }
501     auto pathIdx = static_cast<SubFolderInfo *>(child.internalPointer())->_pathIdx;
502     int i = 1;
503     OC_ASSERT(pathIdx.at(0) < _folders.count());
504     if (pathIdx.count() == 1) {
505         return createIndex(pathIdx.at(0), 0 /*, nullptr*/);
506     }
507 
508     const SubFolderInfo *info = &_folders[pathIdx.at(0)];
509     while (i < pathIdx.count() - 1) {
510         OC_ASSERT(pathIdx.at(i) < info->_subs.count());
511         info = &info->_subs.at(pathIdx.at(i));
512         ++i;
513     }
514     return createIndex(pathIdx.at(i), 0, const_cast<SubFolderInfo *>(info));
515 }
516 
hasChildren(const QModelIndex & parent) const517 bool FolderStatusModel::hasChildren(const QModelIndex &parent) const
518 {
519     if (!parent.isValid())
520         return true;
521 
522     auto info = infoForIndex(parent);
523     if (!info)
524         return false;
525 
526     if (info->_folder && !info->_folder->supportsSelectiveSync())
527         return false;
528 
529     if (!info->_fetched)
530         return true;
531 
532     if (info->_subs.isEmpty())
533         return false;
534 
535     return true;
536 }
537 
538 
canFetchMore(const QModelIndex & parent) const539 bool FolderStatusModel::canFetchMore(const QModelIndex &parent) const
540 {
541     if (!_accountState) {
542         return false;
543     }
544     if (_accountState->state() != AccountState::Connected) {
545         return false;
546     }
547     auto info = infoForIndex(parent);
548     if (!info || info->_fetched || info->_fetchingJob)
549         return false;
550     if (info->_hasError) {
551         // Keep showing the error to the user, it will be hidden when the account reconnects
552         return false;
553     }
554     if (info->_folder && !info->_folder->supportsSelectiveSync()) {
555         // Selective sync is hidden in that case
556         return false;
557     }
558     return true;
559 }
560 
561 
fetchMore(const QModelIndex & parent)562 void FolderStatusModel::fetchMore(const QModelIndex &parent)
563 {
564     auto info = infoForIndex(parent);
565 
566     if (!info || info->_fetched || info->_fetchingJob)
567         return;
568     info->resetSubs(this, parent);
569     QString path = info->_folder->remotePathTrailingSlash();
570     if (info->_path != QLatin1String("/")) {
571         path += info->_path;
572     }
573     LsColJob *job = new LsColJob(_accountState->account(), path, this);
574     info->_fetchingJob = job;
575     job->setProperties(QList<QByteArray>() << "resourcetype"
576                                            << "http://owncloud.org/ns:size"
577                                            << "http://owncloud.org/ns:permissions");
578     job->setTimeout(60 * 1000);
579     connect(job, &LsColJob::directoryListingSubfolders,
580         this, &FolderStatusModel::slotUpdateDirectories);
581     connect(job, &LsColJob::finishedWithError,
582         this, &FolderStatusModel::slotLscolFinishedWithError);
583     connect(job, &LsColJob::directoryListingIterated,
584         this, &FolderStatusModel::slotGatherPermissions);
585 
586     job->start();
587 
588     QPersistentModelIndex persistentIndex(parent);
589     job->setProperty(propertyParentIndexC, QVariant::fromValue(persistentIndex));
590 
591     // Show 'fetching data...' hint after a while.
592     _fetchingItems[persistentIndex].start();
593     QTimer::singleShot(1000, this, &FolderStatusModel::slotShowFetchProgress);
594 }
595 
resetAndFetch(const QModelIndex & parent)596 void FolderStatusModel::resetAndFetch(const QModelIndex &parent)
597 {
598     auto info = infoForIndex(parent);
599     info->resetSubs(this, parent);
600     fetchMore(parent);
601 }
602 
slotGatherPermissions(const QString & href,const QMap<QString,QString> & map)603 void FolderStatusModel::slotGatherPermissions(const QString &href, const QMap<QString, QString> &map)
604 {
605     auto it = map.find("permissions");
606     if (it == map.end())
607         return;
608 
609     auto job = sender();
610     auto permissionMap = job->property(propertyPermissionMap).toMap();
611     job->setProperty(propertyPermissionMap, QVariant()); // avoid a detach of the map while it is modified
612     OC_ASSERT_X(!href.endsWith(QLatin1Char('/')), "LsColXMLParser::parse should remove the trailing slash before calling us.");
613     permissionMap[href] = *it;
614     job->setProperty(propertyPermissionMap, permissionMap);
615 }
616 
slotUpdateDirectories(const QStringList & list)617 void FolderStatusModel::slotUpdateDirectories(const QStringList &list)
618 {
619     auto job = qobject_cast<LsColJob *>(sender());
620     OC_ASSERT(job);
621     QModelIndex idx = qvariant_cast<QPersistentModelIndex>(job->property(propertyParentIndexC));
622     auto parentInfo = infoForIndex(idx);
623     if (!parentInfo) {
624         return;
625     }
626     if (!parentInfo->_folder->supportsSelectiveSync()) {
627         return;
628     }
629     OC_ASSERT(parentInfo->_fetchingJob == job);
630     OC_ASSERT(parentInfo->_subs.isEmpty());
631 
632     if (parentInfo->hasLabel()) {
633         beginRemoveRows(idx, 0, 0);
634         parentInfo->_hasError = false;
635         parentInfo->_fetchingLabel = false;
636         endRemoveRows();
637     }
638 
639     parentInfo->_lastErrorString.clear();
640     parentInfo->_fetchingJob = nullptr;
641     parentInfo->_fetched = true;
642 
643     QUrl url = parentInfo->_folder->remoteUrl();
644     QString pathToRemove = url.path();
645     if (!pathToRemove.endsWith('/'))
646         pathToRemove += '/';
647 
648     QStringList selectiveSyncBlackList;
649     bool ok1 = true;
650     bool ok2 = true;
651     if (parentInfo->_checked == Qt::PartiallyChecked) {
652         selectiveSyncBlackList = parentInfo->_folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok1);
653     }
654     auto selectiveSyncUndecidedList = parentInfo->_folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &ok2);
655 
656     if (!(ok1 && ok2)) {
657         qCWarning(lcFolderStatus) << "Could not retrieve selective sync info from journal";
658         return;
659     }
660 
661     std::set<QString> selectiveSyncUndecidedSet; // not QSet because it's not sorted
662     foreach (const QString &str, selectiveSyncUndecidedList) {
663         if (str.startsWith(parentInfo->_path) || parentInfo->_path == QLatin1String("/")) {
664             selectiveSyncUndecidedSet.insert(str);
665         }
666     }
667     const auto permissionMap = job->property(propertyPermissionMap).toMap();
668 
669     QStringList sortedSubfolders = list;
670     if (!sortedSubfolders.isEmpty())
671         sortedSubfolders.removeFirst(); // skip the parent item (first in the list)
672     Utility::sortFilenames(sortedSubfolders);
673 
674     QVarLengthArray<int, 10> undecidedIndexes;
675 
676     QVector<SubFolderInfo> newSubs;
677     newSubs.reserve(sortedSubfolders.size());
678     foreach (const QString &path, sortedSubfolders) {
679         auto relativePath = path.mid(pathToRemove.size());
680         if (parentInfo->_folder->isFileExcludedRelative(relativePath)) {
681             continue;
682         }
683 
684         SubFolderInfo newInfo;
685         newInfo._folder = parentInfo->_folder;
686         newInfo._pathIdx = parentInfo->_pathIdx;
687         newInfo._pathIdx << newSubs.size();
688         newInfo._size = job->_sizes.value(path);
689         newInfo._isExternal = permissionMap.value(removeTrailingSlash(path)).toString().contains("M");
690         newInfo._path = relativePath;
691         newInfo._name = removeTrailingSlash(relativePath).split('/').last();
692 
693         if (relativePath.isEmpty())
694             continue;
695 
696         if (parentInfo->_checked == Qt::Unchecked) {
697             newInfo._checked = Qt::Unchecked;
698         } else if (parentInfo->_checked == Qt::Checked) {
699             newInfo._checked = Qt::Checked;
700         } else {
701             foreach (const QString &str, selectiveSyncBlackList) {
702                 if (str == relativePath || str == QLatin1String("/")) {
703                     newInfo._checked = Qt::Unchecked;
704                     break;
705                 } else if (str.startsWith(relativePath)) {
706                     newInfo._checked = Qt::PartiallyChecked;
707                 }
708             }
709         }
710 
711         auto it = selectiveSyncUndecidedSet.lower_bound(relativePath);
712         if (it != selectiveSyncUndecidedSet.end()) {
713             if (*it == relativePath) {
714                 newInfo._isUndecided = true;
715                 selectiveSyncUndecidedSet.erase(it);
716             } else if ((*it).startsWith(relativePath)) {
717                 undecidedIndexes.append(newInfo._pathIdx.last());
718 
719                 // Remove all the items from the selectiveSyncUndecidedSet that starts with this path
720                 QString relativePathNext = relativePath;
721                 relativePathNext[relativePathNext.length() - 1].unicode()++;
722                 auto it2 = selectiveSyncUndecidedSet.lower_bound(relativePathNext);
723                 selectiveSyncUndecidedSet.erase(it, it2);
724             }
725         }
726         newSubs.append(newInfo);
727     }
728 
729     if (!newSubs.isEmpty()) {
730         beginInsertRows(idx, 0, newSubs.size() - 1);
731         parentInfo->_subs = std::move(newSubs);
732         endInsertRows();
733     }
734 
735     for (auto it = undecidedIndexes.begin(); it != undecidedIndexes.end(); ++it) {
736         suggestExpand(index(*it, 0, idx));
737     }
738     /* Try to remove the the undecided lists the items that are not on the server. */
739     auto it = std::remove_if(selectiveSyncUndecidedList.begin(), selectiveSyncUndecidedList.end(),
740         [&](const QString &s) { return selectiveSyncUndecidedSet.count(s); });
741     if (it != selectiveSyncUndecidedList.end()) {
742         selectiveSyncUndecidedList.erase(it, selectiveSyncUndecidedList.end());
743         parentInfo->_folder->journalDb()->setSelectiveSyncList(
744             SyncJournalDb::SelectiveSyncUndecidedList, selectiveSyncUndecidedList);
745         emit dirtyChanged();
746     }
747 }
748 
slotLscolFinishedWithError(QNetworkReply * r)749 void FolderStatusModel::slotLscolFinishedWithError(QNetworkReply *r)
750 {
751     auto job = qobject_cast<LsColJob *>(sender());
752     OC_ASSERT(job);
753     QModelIndex idx = qvariant_cast<QPersistentModelIndex>(job->property(propertyParentIndexC));
754     if (!idx.isValid()) {
755         return;
756     }
757     auto parentInfo = infoForIndex(idx);
758     if (parentInfo) {
759         qCDebug(lcFolderStatus) << r->errorString();
760         parentInfo->_lastErrorString = r->errorString();
761         auto error = r->error();
762 
763         parentInfo->resetSubs(this, idx);
764 
765         if (error == QNetworkReply::ContentNotFoundError) {
766             parentInfo->_fetched = true;
767         } else {
768             OC_ASSERT(!parentInfo->hasLabel());
769             beginInsertRows(idx, 0, 0);
770             parentInfo->_hasError = true;
771             endInsertRows();
772         }
773     }
774 }
775 
createBlackList(const FolderStatusModel::SubFolderInfo & root,const QStringList & oldBlackList) const776 QStringList FolderStatusModel::createBlackList(const FolderStatusModel::SubFolderInfo &root,
777     const QStringList &oldBlackList) const
778 {
779     switch (root._checked) {
780     case Qt::Unchecked:
781         return QStringList(root._path);
782     case Qt::Checked:
783         return QStringList();
784     case Qt::PartiallyChecked:
785         break;
786     }
787 
788     QStringList result;
789     if (root._fetched) {
790         for (int i = 0; i < root._subs.count(); ++i) {
791             result += createBlackList(root._subs.at(i), oldBlackList);
792         }
793     } else {
794         // We did not load from the server so we re-use the one from the old black list
795         const QString path = root._path;
796         foreach (const QString &it, oldBlackList) {
797             if (it.startsWith(path))
798                 result += it;
799         }
800     }
801     return result;
802 }
803 
slotUpdateFolderState(Folder * folder)804 void FolderStatusModel::slotUpdateFolderState(Folder *folder)
805 {
806     if (!folder)
807         return;
808     for (int i = 0; i < _folders.count(); ++i) {
809         if (_folders.at(i)._folder == folder) {
810             emit dataChanged(index(i), index(i));
811         }
812     }
813 }
814 
slotApplySelectiveSync()815 void FolderStatusModel::slotApplySelectiveSync()
816 {
817     for (const auto &folderInfo : qAsConst(_folders)) {
818         if (!folderInfo._fetched) {
819             folderInfo._folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, QStringList());
820             continue;
821         }
822         const auto folder = folderInfo._folder;
823 
824         bool ok;
825         auto oldBlackList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
826         if (!ok) {
827             qCWarning(lcFolderStatus) << "Could not read selective sync list from db.";
828             continue;
829         }
830         QStringList blackList = createBlackList(folderInfo, oldBlackList);
831         folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList);
832 
833         auto blackListSet = blackList.toSet();
834         auto oldBlackListSet = oldBlackList.toSet();
835 
836         // The folders that were undecided or blacklisted and that are now checked should go on the white list.
837         // The user confirmed them already just now.
838         QStringList toAddToWhiteList = ((oldBlackListSet + folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &ok).toSet()) - blackListSet).toList();
839 
840         if (!toAddToWhiteList.isEmpty()) {
841             auto whiteList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, &ok);
842             if (ok) {
843                 whiteList += toAddToWhiteList;
844                 folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, whiteList);
845             }
846         }
847         // clear the undecided list
848         folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, QStringList());
849 
850         // do the sync if there were changes
851         auto changes = (oldBlackListSet - blackListSet) + (blackListSet - oldBlackListSet);
852         if (!changes.isEmpty()) {
853             if (folder->isBusy()) {
854                 folder->slotTerminateSync();
855             }
856             //The part that changed should not be read from the DB on next sync because there might be new folders
857             // (the ones that are no longer in the blacklist)
858             foreach (const auto &it, changes) {
859                 folder->journalDb()->schedulePathForRemoteDiscovery(it);
860                 folder->schedulePathForLocalDiscovery(it);
861             }
862             // Also make sure we see the local file that had been ignored before
863             folder->slotNextSyncFullLocalDiscovery();
864             FolderMan::instance()->scheduleFolder(folder);
865         }
866     }
867 
868     resetFolders();
869 }
870 
slotSetProgress(const ProgressInfo & progress)871 void FolderStatusModel::slotSetProgress(const ProgressInfo &progress)
872 {
873     auto par = qobject_cast<QWidget *>(QObject::parent());
874     if (!par->isVisible()) {
875         return; // for https://github.com/owncloud/client/issues/2648#issuecomment-71377909
876     }
877 
878     Folder *f = qobject_cast<Folder *>(sender());
879     if (!f) {
880         return;
881     }
882 
883     int folderIndex = -1;
884     for (int i = 0; i < _folders.count(); ++i) {
885         if (_folders.at(i)._folder == f) {
886             folderIndex = i;
887             break;
888         }
889     }
890     if (folderIndex < 0) {
891         return;
892     }
893 
894     auto *pi = &_folders[folderIndex]._progress;
895 
896     QVector<int> roles;
897     roles << FolderStatusDelegate::SyncProgressItemString
898           << FolderStatusDelegate::WarningCount
899           << Qt::ToolTipRole;
900 
901     if (progress.status() == ProgressInfo::Discovery) {
902         if (!progress._currentDiscoveredRemoteFolder.isEmpty()) {
903             pi->_overallSyncString = tr("Checking for changes in remote '%1'").arg(progress._currentDiscoveredRemoteFolder);
904             emit dataChanged(index(folderIndex), index(folderIndex), roles);
905             return;
906         } else if (!progress._currentDiscoveredLocalFolder.isEmpty()) {
907             pi->_overallSyncString = tr("Checking for changes in local '%1'").arg(progress._currentDiscoveredLocalFolder);
908             emit dataChanged(index(folderIndex), index(folderIndex), roles);
909             return;
910         }
911     }
912 
913     if (progress.status() == ProgressInfo::Reconcile) {
914         pi->_overallSyncString = tr("Reconciling changes");
915         emit dataChanged(index(folderIndex), index(folderIndex), roles);
916         return;
917     }
918 
919     // Status is Starting, Propagation or Done
920 
921     if (!progress._lastCompletedItem.isEmpty()
922         && Progress::isWarningKind(progress._lastCompletedItem._status)) {
923         pi->_warningCount++;
924     }
925 
926     // find the single item to display:  This is going to be the bigger item, or the last completed
927     // item if no items are in progress.
928     SyncFileItem curItem = progress._lastCompletedItem;
929     qint64 curItemProgress = -1; // -1 means finished
930     qint64 biggerItemSize = 0;
931     quint64 estimatedUpBw = 0;
932     quint64 estimatedDownBw = 0;
933     QString allFilenames;
934     foreach (const ProgressInfo::ProgressItem &citm, progress._currentItems) {
935         if (curItemProgress == -1 || (ProgressInfo::isSizeDependent(citm._item)
936                                          && biggerItemSize < citm._item._size)) {
937             curItemProgress = citm._progress.completed();
938             curItem = citm._item;
939             biggerItemSize = citm._item._size;
940         }
941         if (citm._item._direction != SyncFileItem::Up) {
942             estimatedDownBw += progress.fileProgress(citm._item).estimatedBandwidth;
943         } else {
944             estimatedUpBw += progress.fileProgress(citm._item).estimatedBandwidth;
945         }
946         auto fileName = QFileInfo(citm._item._file).fileName();
947         if (allFilenames.length() > 0) {
948             //: Build a list of file names
949             allFilenames.append(tr(", '%1'").arg(fileName));
950         } else {
951             //: Argument is a file name
952             allFilenames.append(tr("'%1'").arg(fileName));
953         }
954     }
955     if (curItemProgress == -1) {
956         curItemProgress = curItem._size;
957     }
958 
959     QString itemFileName = curItem._file;
960     QString kindString = Progress::asActionString(curItem);
961 
962     QString fileProgressString;
963     if (ProgressInfo::isSizeDependent(curItem)) {
964         QString s1 = Utility::octetsToString(curItemProgress);
965         QString s2 = Utility::octetsToString(curItem._size);
966         //quint64 estimatedBw = progress.fileProgress(curItem).estimatedBandwidth;
967         if (estimatedUpBw || estimatedDownBw) {
968             /*
969             //: Example text: "uploading foobar.png (1MB of 2MB) time left 2 minutes at a rate of 24Kb/s"
970             fileProgressString = tr("%1 %2 (%3 of %4) %5 left at a rate of %6/s")
971                 .arg(kindString, itemFileName, s1, s2,
972                     Utility::durationToDescriptiveString(progress.fileProgress(curItem).estimatedEta),
973                     Utility::octetsToString(estimatedBw) );
974             */
975             //: Example text: "Syncing 'foo.txt', 'bar.txt'"
976             fileProgressString = tr("Syncing %1").arg(allFilenames);
977             if (estimatedDownBw > 0) {
978                 fileProgressString.append(tr(", "));
979 // ifdefs: https://github.com/owncloud/client/issues/3095#issuecomment-128409294
980 #ifdef Q_OS_WIN
981                 //: Example text: "download 24Kb/s"   (%1 is replaced by 24Kb (translated))
982                 fileProgressString.append(tr("download %1/s").arg(Utility::octetsToString(estimatedDownBw)));
983 #else
984                 fileProgressString.append(tr("\u2193 %1/s")
985                                               .arg(Utility::octetsToString(estimatedDownBw)));
986 #endif
987             }
988             if (estimatedUpBw > 0) {
989                 fileProgressString.append(tr(", "));
990 #ifdef Q_OS_WIN
991                 //: Example text: "upload 24Kb/s"   (%1 is replaced by 24Kb (translated))
992                 fileProgressString.append(tr("upload %1/s").arg(Utility::octetsToString(estimatedUpBw)));
993 #else
994                 fileProgressString.append(tr("\u2191 %1/s")
995                                               .arg(Utility::octetsToString(estimatedUpBw)));
996 #endif
997             }
998         } else {
999             //: Example text: "uploading foobar.png (2MB of 2MB)"
1000             fileProgressString = tr("%1 %2 (%3 of %4)").arg(kindString, itemFileName, s1, s2);
1001         }
1002     } else if (!kindString.isEmpty()) {
1003         //: Example text: "uploading foobar.png"
1004         fileProgressString = tr("%1 %2").arg(kindString, itemFileName);
1005     }
1006     pi->_progressString = fileProgressString;
1007 
1008     // overall progress
1009     qint64 completedSize = progress.completedSize();
1010     qint64 completedFile = progress.completedFiles();
1011     qint64 currentFile = progress.currentFile();
1012     qint64 totalSize = qMax(completedSize, progress.totalSize());
1013     qint64 totalFileCount = qMax(currentFile, progress.totalFiles());
1014     QString overallSyncString;
1015     if (totalSize > 0) {
1016         QString s1 = Utility::octetsToString(completedSize);
1017         QString s2 = Utility::octetsToString(totalSize);
1018 
1019         if (progress.trustEta()) {
1020             //: Example text: "5 minutes left, 12 MB of 345 MB, file 6 of 7"
1021             overallSyncString = tr("%5 left, %1 of %2, file %3 of %4")
1022                                     .arg(s1, s2)
1023                                     .arg(currentFile)
1024                                     .arg(totalFileCount)
1025                                     .arg(Utility::durationToDescriptiveString1(progress.totalProgress().estimatedEta));
1026 
1027         } else {
1028             //: Example text: "12 MB of 345 MB, file 6 of 7"
1029             overallSyncString = tr("%1 of %2, file %3 of %4")
1030                                     .arg(s1, s2)
1031                                     .arg(currentFile)
1032                                     .arg(totalFileCount);
1033         }
1034     } else if (totalFileCount > 0) {
1035         // Don't attempt to estimate the time left if there is no kb to transfer.
1036         overallSyncString = tr("file %1 of %2").arg(currentFile).arg(totalFileCount);
1037     }
1038 
1039     pi->_overallSyncString = overallSyncString;
1040 
1041     int overallPercent = 0;
1042     if (totalFileCount > 0) {
1043         // Add one 'byte' for each file so the percentage is moving when deleting or renaming files
1044         overallPercent = qRound(double(completedSize + completedFile) / double(totalSize + totalFileCount) * 100.0);
1045     }
1046     pi->_overallPercent = qBound(0, overallPercent, 100);
1047     emit dataChanged(index(folderIndex), index(folderIndex), roles);
1048 }
1049 
slotFolderSyncStateChange(Folder * f)1050 void FolderStatusModel::slotFolderSyncStateChange(Folder *f)
1051 {
1052     if (!f) {
1053         return;
1054     }
1055 
1056     int folderIndex = -1;
1057     for (int i = 0; i < _folders.count(); ++i) {
1058         if (_folders.at(i)._folder == f) {
1059             folderIndex = i;
1060             break;
1061         }
1062     }
1063     if (folderIndex < 0) {
1064         return;
1065     }
1066 
1067     auto &pi = _folders[folderIndex]._progress;
1068 
1069     SyncResult::Status state = f->syncResult().status();
1070     if (!f->canSync()) {
1071         // Reset progress info.
1072         pi = SubFolderInfo::Progress();
1073     } else if (state == SyncResult::NotYetStarted) {
1074         FolderMan *folderMan = FolderMan::instance();
1075         int pos = folderMan->scheduleQueue().indexOf(f);
1076         for (auto other : folderMan->map()) {
1077             if (other != f && other->isSyncRunning())
1078                 pos += 1;
1079         }
1080         QString message;
1081         if (pos <= 0) {
1082             message = tr("Waiting...");
1083         } else {
1084             message = tr("Waiting for %n other folder(s)...", "", pos);
1085         }
1086         pi = SubFolderInfo::Progress();
1087         pi._overallSyncString = message;
1088     } else if (state == SyncResult::SyncPrepare) {
1089         pi = SubFolderInfo::Progress();
1090         pi._overallSyncString = tr("Preparing to sync...");
1091     } else if (state == SyncResult::Problem || state == SyncResult::Success) {
1092         // Reset the progress info after a sync.
1093         pi = SubFolderInfo::Progress();
1094     } else if (state == SyncResult::Error) {
1095         pi = SubFolderInfo::Progress();
1096     }
1097 
1098     // update the icon etc. now
1099     slotUpdateFolderState(f);
1100 
1101     if (f->syncResult().folderStructureWasChanged()
1102         && (state == SyncResult::Success || state == SyncResult::Problem)) {
1103         // There is a new or a removed folder. reset all data
1104         resetAndFetch(index(folderIndex));
1105     }
1106 }
1107 
slotFolderScheduleQueueChanged()1108 void FolderStatusModel::slotFolderScheduleQueueChanged()
1109 {
1110     // Update messages on waiting folders.
1111     foreach (Folder *f, FolderMan::instance()->map()) {
1112         slotFolderSyncStateChange(f);
1113     }
1114 }
1115 
resetFolders()1116 void FolderStatusModel::resetFolders()
1117 {
1118     setAccountState(_accountState);
1119 }
1120 
slotSyncAllPendingBigFolders()1121 void FolderStatusModel::slotSyncAllPendingBigFolders()
1122 {
1123     for (int i = 0; i < _folders.count(); ++i) {
1124         if (!_folders[i]._fetched) {
1125             _folders[i]._folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, QStringList());
1126             continue;
1127         }
1128         auto folder = _folders.at(i)._folder;
1129 
1130         bool ok;
1131         auto undecidedList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &ok);
1132         if (!ok) {
1133             qCWarning(lcFolderStatus) << "Could not read selective sync list from db.";
1134             return;
1135         }
1136 
1137         // If this folder had no undecided entries, skip it.
1138         if (undecidedList.isEmpty()) {
1139             continue;
1140         }
1141 
1142         // Remove all undecided folders from the blacklist
1143         auto blackList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
1144         if (!ok) {
1145             qCWarning(lcFolderStatus) << "Could not read selective sync list from db.";
1146             return;
1147         }
1148         foreach (const auto &undecidedFolder, undecidedList) {
1149             blackList.removeAll(undecidedFolder);
1150         }
1151         folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList);
1152 
1153         // Add all undecided folders to the white list
1154         auto whiteList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, &ok);
1155         if (!ok) {
1156             qCWarning(lcFolderStatus) << "Could not read selective sync list from db.";
1157             return;
1158         }
1159         whiteList += undecidedList;
1160         folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, whiteList);
1161 
1162         // Clear the undecided list
1163         folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, QStringList());
1164 
1165         // Trigger a sync
1166         if (folder->isBusy()) {
1167             folder->slotTerminateSync();
1168         }
1169         // The part that changed should not be read from the DB on next sync because there might be new folders
1170         // (the ones that are no longer in the blacklist)
1171         foreach (const auto &it, undecidedList) {
1172             folder->journalDb()->schedulePathForRemoteDiscovery(it);
1173             folder->schedulePathForLocalDiscovery(it);
1174         }
1175         // Also make sure we see the local file that had been ignored before
1176         folder->slotNextSyncFullLocalDiscovery();
1177         FolderMan::instance()->scheduleFolder(folder);
1178     }
1179 
1180     resetFolders();
1181 }
1182 
slotSyncNoPendingBigFolders()1183 void FolderStatusModel::slotSyncNoPendingBigFolders()
1184 {
1185     for (int i = 0; i < _folders.count(); ++i) {
1186         auto folder = _folders.at(i)._folder;
1187 
1188         // clear the undecided list
1189         folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, QStringList());
1190     }
1191 
1192     resetFolders();
1193 }
1194 
slotNewBigFolder()1195 void FolderStatusModel::slotNewBigFolder()
1196 {
1197     auto f = qobject_cast<Folder *>(sender());
1198     OC_ASSERT(f);
1199 
1200     int folderIndex = -1;
1201     for (int i = 0; i < _folders.count(); ++i) {
1202         if (_folders.at(i)._folder == f) {
1203             folderIndex = i;
1204             break;
1205         }
1206     }
1207     if (folderIndex < 0) {
1208         return;
1209     }
1210 
1211     resetAndFetch(index(folderIndex));
1212 
1213     emit suggestExpand(index(folderIndex));
1214     emit dirtyChanged();
1215 }
1216 
slotShowFetchProgress()1217 void FolderStatusModel::slotShowFetchProgress()
1218 {
1219     QMutableMapIterator<QPersistentModelIndex, QElapsedTimer> it(_fetchingItems);
1220     while (it.hasNext()) {
1221         it.next();
1222         if (it.value().elapsed() > 800) {
1223             auto idx = it.key();
1224             auto *info = infoForIndex(idx);
1225             if (info && info->_fetchingJob) {
1226                 bool add = !info->hasLabel();
1227                 if (add) {
1228                     beginInsertRows(idx, 0, 0);
1229                 }
1230                 info->_fetchingLabel = true;
1231                 if (add) {
1232                     endInsertRows();
1233                 }
1234             }
1235             it.remove();
1236         }
1237     }
1238 }
1239 
hasLabel() const1240 bool FolderStatusModel::SubFolderInfo::hasLabel() const
1241 {
1242     return _hasError || _fetchingLabel;
1243 }
1244 
resetSubs(FolderStatusModel * model,QModelIndex index)1245 void FolderStatusModel::SubFolderInfo::resetSubs(FolderStatusModel *model, QModelIndex index)
1246 {
1247     _fetched = false;
1248     if (_fetchingJob) {
1249         disconnect(_fetchingJob, nullptr, model, nullptr);
1250         _fetchingJob->deleteLater();
1251         _fetchingJob.clear();
1252     }
1253     if (hasLabel()) {
1254         model->beginRemoveRows(index, 0, 0);
1255         _fetchingLabel = false;
1256         _hasError = false;
1257         model->endRemoveRows();
1258     } else if (!_subs.isEmpty()) {
1259         model->beginRemoveRows(index, 0, _subs.count() - 1);
1260         _subs.clear();
1261         model->endRemoveRows();
1262     }
1263 }
1264 
1265 
1266 } // namespace OCC
1267