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