1 #include <QTimer>
2 #include <QHash>
3 #include <QDebug>
4 #include <algorithm>            // std::sort
5 
6 #include "api/server-repo.h"
7 
8 #include "utils/utils.h"
9 #include "seafile-applet.h"
10 #include "account-mgr.h"
11 #include "main-window.h"
12 #include "rpc/rpc-client.h"
13 #include "rpc/clone-task.h"
14 #include "repo-service.h"
15 
16 #include "repo-item.h"
17 #include "repo-tree-view.h"
18 #include "repo-tree-model.h"
19 
20 namespace {
21 
22 const int kRefreshLocalReposInterval = 1000;
23 const int kMaxRecentUpdatedRepos = 10;
24 const int kIndexOfVirtualReposCategory = 2;
25 const char *kReadonlyPropertyName = "is-readonly";
26 
compareRepoByTimestamp(const ServerRepo & a,const ServerRepo & b)27 bool compareRepoByTimestamp(const ServerRepo& a, const ServerRepo& b)
28 {
29     return a.mtime > b.mtime;
30 }
31 
makeFilterRegExp(const QString & text)32 QRegExp makeFilterRegExp(const QString& text)
33 {
34     return QRegExp(text.split(" ", QString::SkipEmptyParts).join(".*"),
35                    Qt::CaseInsensitive);
36 }
37 
38 
39 } // namespace
40 
41 
RepoTreeModel(QObject * parent)42 RepoTreeModel::RepoTreeModel(QObject *parent)
43     : QStandardItemModel(parent),
44       tree_view_(NULL)
45 {
46     initialize();
47 
48     refresh_local_timer_ = new QTimer(this);
49     connect(refresh_local_timer_, SIGNAL(timeout()),
50             this, SLOT(refreshLocalRepos()));
51 
52     refresh_local_timer_->start(kRefreshLocalReposInterval);
53 }
54 
~RepoTreeModel()55 RepoTreeModel::~RepoTreeModel()
56 {
57     if (item(kIndexOfVirtualReposCategory) != virtual_repos_category_)
58         delete virtual_repos_category_;
59 }
60 
initialize()61 void RepoTreeModel::initialize()
62 {
63     recent_updated_category_ = new RepoCategoryItem(CAT_INDEX_RECENT_UPDATED, tr("Recently Updated"));
64     my_repos_category_ = new RepoCategoryItem(CAT_INDEX_MY_REPOS, tr("My Libraries"));
65     virtual_repos_category_ = new RepoCategoryItem(CAT_INDEX_VIRTUAL_REPOS, tr("Sub Libraries"));
66     shared_repos_category_ = new RepoCategoryItem(CAT_INDEX_SHARED_REPOS, tr("Shared with me"));
67     org_repos_category_ = new RepoCategoryItem(CAT_INDEX_SHARED_REPOS, tr("Shared with all"));
68     groups_root_category_ = new RepoCategoryItem(CAT_INDEX_GROUP_REPOS, tr("Shared with groups"), 0);
69     synced_repos_category_ = new RepoCategoryItem(CAT_INDEX_SYNCED_REPOS, tr("Synced Libraries"));
70 
71     appendRow(recent_updated_category_);
72     appendRow(my_repos_category_);
73     // appendRow(virtual_repos_category_);
74     appendRow(shared_repos_category_);
75     appendRow(groups_root_category_);
76     appendRow(synced_repos_category_);
77 
78     if (tree_view_) {
79         tree_view_->restoreExpandedCategries();
80     }
81 }
82 
proxiedIndexFromItem(const QStandardItem * item)83 QModelIndex RepoTreeModel::proxiedIndexFromItem(const QStandardItem* item)
84 {
85     QSortFilterProxyModel *proxy_model = (QSortFilterProxyModel *)tree_view_->model();
86     return proxy_model->mapFromSource(indexFromItem(item));
87 }
88 
clear()89 void RepoTreeModel::clear()
90 {
91     beginResetModel();
92     QStandardItemModel::clear();
93     initialize();
94     endResetModel();
95 }
96 
setRepos(const std::vector<ServerRepo> & repos)97 void RepoTreeModel::setRepos(const std::vector<ServerRepo>& repos)
98 {
99     size_t i, n = repos.size();
100     // removeReposDeletedOnServer(repos);
101 
102     clear();
103 
104     QHash<QString, ServerRepo> map;
105     const Account& account = seafApplet->accountManager()->currentAccount();
106 
107     QHash<QString, QString> merged_repo_perms;
108     // If a repo is shared read-only to org and read-write to me (or vice
109     // versa), the final permission should be read-write.
110     for (i = 0; i < n; i++) {
111         ServerRepo repo = repos[i];
112         QString exist_perm, current_perm;
113 
114         if (merged_repo_perms.contains(repo.id)) {
115             exist_perm = merged_repo_perms[repo.id];
116         }
117 
118         current_perm = repo.permission;
119         if (repo.owner == account.username && repo.permission == "r") {
120             // When the user shares a repo to organization with read-only
121             // permission, the returned repo has permission set to "r", we need
122             // to fix it.
123             current_perm = "rw";
124         }
125 
126         if (current_perm == "rw" || exist_perm == "rw") {
127             merged_repo_perms[repo.id] = "rw";
128         } else {
129             merged_repo_perms[repo.id] = "r";
130         }
131 
132         // printf("repo: %s, exist_perm: %s, current_perm: %s, final_perm: %s\n",
133         //        toCStr(repo.name),
134         //        toCStr(exist_perm),
135         //        toCStr(current_perm),
136         //        toCStr(merged_repo_perms[repo.id]));
137     }
138 
139     for (i = 0; i < n; i++) {
140         ServerRepo repo = repos[i];
141         repo.permission = merged_repo_perms[repo.id];
142         // TODO: repo.readonly totally depends on repo.permission, so it should
143         // be a function instead of a variable.
144         repo.readonly = repo.permission == "r";
145 
146         if (repo.isPersonalRepo()) {
147             if (!repo.isSubfolder() && !repo.isVirtual()) {
148                 checkPersonalRepo(repo);
149             }
150         } else if (repo.isSharedRepo()) {
151             checkSharedRepo(repo);
152         } else if (repo.isOrgRepo()) {
153             checkOrgRepo(repo);
154         } else {
155             checkGroupRepo(repo);
156         }
157 
158         if (repo.isSubfolder() || seafApplet->rpcClient()->hasLocalRepo(repo.id))
159             checkSyncedRepo(repo);
160 
161         // we have a conflicting case, don't use group version if we can
162         if (map.contains(repo.id) && repo.isGroupRepo())
163             continue;
164         map[repo.id] = repo;
165     }
166 
167     QList<ServerRepo> list = map.values();
168     // sort all repos by timestamp
169     // use std::sort for qt containers will force additional copy.
170     // anyway, we can use qt's alternative qSort for it
171     std::stable_sort(list.begin(), list.end(), compareRepoByTimestamp);
172 
173     n = qMin(list.size(), kMaxRecentUpdatedRepos);
174     for (i = 0; i < n; i++) {
175         RepoItem *item = new RepoItem(list[i]);
176         recent_updated_category_->appendRow(item);
177     }
178     updateLocalReposPerm(list);
179 }
180 
updateLocalReposPerm(const QList<ServerRepo> & repos)181 void RepoTreeModel::updateLocalReposPerm(const QList<ServerRepo> &repos)
182 {
183     int n = repos.size();
184     for (int i = 0; i < n; i++) {
185         ServerRepo repo = repos[i];
186         if (seafApplet->rpcClient()->hasLocalRepo(repo.id)) {
187             QString readonly_prop;
188             if (seafApplet->rpcClient()->getRepoProperty(
189                     repo.id, kReadonlyPropertyName, &readonly_prop) < 0) {
190                 continue;
191             }
192             bool readonly = readonly_prop == "true";
193             if (repo.readonly != readonly) {
194                 seafApplet->rpcClient()->setRepoProperty(
195                     repo.id,
196                     kReadonlyPropertyName,
197                     repo.readonly ? "true" : "false");
198                 qWarning("repo %s %s permission changed to %s",
199                          toCStr(repo.id),
200                          toCStr(repo.name.left(40)),
201                          toCStr(repo.permission));
202             }
203         }
204     }
205 }
206 
207 struct DeleteRepoData {
208     QHash<QString, const ServerRepo*> map;
209     QList<RepoItem*> itemsToDelete;
210 };
211 
collectDeletedRepos(RepoItem * item,void * vdata)212 void RepoTreeModel::collectDeletedRepos(RepoItem *item, void *vdata)
213 {
214     DeleteRepoData *data = (DeleteRepoData *)vdata;
215     const ServerRepo* repo = data->map.value(item->repo().id);
216     if (!repo || repo->type != item->repo().type) {
217         data->itemsToDelete << item;
218     }
219 }
220 
removeReposDeletedOnServer(const std::vector<ServerRepo> & repos)221 void RepoTreeModel::removeReposDeletedOnServer(const std::vector<ServerRepo>& repos)
222 {
223     int i, n;
224     DeleteRepoData data;
225     n = repos.size();
226     for (i = 0; i < n; i++) {
227         const ServerRepo& repo = repos[i];
228         data.map.insert(repo.id, &repo);
229     }
230 
231     forEachRepoItem(&RepoTreeModel::collectDeletedRepos, (void *)&data);
232 
233     QListIterator<RepoItem*> iter(data.itemsToDelete);
234     while(iter.hasNext()) {
235         RepoItem *item = iter.next();
236 
237         const ServerRepo& repo = item->repo();
238 
239         qDebug("remove repo %s(%s) from \"%s\"\n",
240                toCStr(repo.name), toCStr(repo.id),
241                toCStr(((RepoCategoryItem*)item->parent())->name()));
242 
243         item->parent()->removeRow(item->row());
244     }
245 }
246 
247 
checkPersonalRepo(const ServerRepo & repo)248 void RepoTreeModel::checkPersonalRepo(const ServerRepo& repo)
249 {
250     int row, n = my_repos_category_->rowCount();
251     for (row = 0; row < n; row++) {
252         RepoItem *item = (RepoItem *)(my_repos_category_->child(row));
253         if (item->repo().id == repo.id) {
254             updateRepoItem(item, repo);
255             return;
256         }
257     }
258 
259     // The repo is new
260     RepoItem *item = new RepoItem(repo);
261     my_repos_category_->appendRow(item);
262 }
263 
checkVirtualRepo(const ServerRepo & repo)264 void RepoTreeModel::checkVirtualRepo(const ServerRepo& repo)
265 {
266     if (item(kIndexOfVirtualReposCategory) != virtual_repos_category_) {
267         insertRow(kIndexOfVirtualReposCategory, virtual_repos_category_);
268     }
269 
270     int row, n = virtual_repos_category_->rowCount();
271     for (row = 0; row < n; row++) {
272         RepoItem *item = (RepoItem *)(virtual_repos_category_->child(row));
273         if (item->repo().id == repo.id) {
274             updateRepoItem(item, repo);
275             return;
276         }
277     }
278 
279     // The repo is new
280     RepoItem *item = new RepoItem(repo);
281     virtual_repos_category_->appendRow(item);
282 }
283 
checkSharedRepo(const ServerRepo & repo)284 void RepoTreeModel::checkSharedRepo(const ServerRepo& repo)
285 {
286     int row, n = shared_repos_category_->rowCount();
287     for (row = 0; row < n; row++) {
288         RepoItem *item = (RepoItem *)(shared_repos_category_->child(row));
289         if (item->repo().id == repo.id) {
290             updateRepoItem(item, repo);
291             return;
292         }
293     }
294 
295     // the repo is a new one
296     RepoItem *item = new RepoItem(repo);
297     shared_repos_category_->appendRow(item);
298 }
299 
checkOrgRepo(const ServerRepo & repo)300 void RepoTreeModel::checkOrgRepo(const ServerRepo& repo)
301 {
302     int row, n = org_repos_category_->rowCount();
303     if (invisibleRootItem()->child(3) != org_repos_category_) {
304         // Insert pub repos after "recent updated", "my libraries", "shared libraries"
305         insertRow(3, org_repos_category_);
306     }
307     for (row = 0; row < n; row++) {
308         RepoItem *item = (RepoItem *)(org_repos_category_->child(row));
309         if (item->repo().id == repo.id) {
310             updateRepoItem(item, repo);
311             return;
312         }
313     }
314 
315     // the repo is a new one
316     RepoItem *item = new RepoItem(repo);
317     org_repos_category_->appendRow(item);
318 }
319 
checkGroupRepo(const ServerRepo & repo)320 void RepoTreeModel::checkGroupRepo(const ServerRepo &repo)
321 {
322     RepoCategoryItem *group = NULL;
323     int row, n = groups_root_category_->rowCount();
324 
325     for (row = 0; row < n; row++) {
326         RepoCategoryItem *item =
327             (RepoCategoryItem *)(groups_root_category_->child(row));
328         if (item->groupId() == repo.group_id) {
329             group = item;
330             break;
331         }
332     }
333 
334     if (!group) {
335         group = new RepoCategoryItem(
336             CAT_INDEX_GROUP_REPOS, repo.group_name, repo.group_id);
337         group->setLevel(1);
338         groups_root_category_->appendRow(group);
339     }
340 
341     // Find the repo in this group
342     n = group->rowCount();
343     for (row = 0; row < n; row++) {
344         RepoItem *item = (RepoItem *)(group->child(row));
345         if (item->repo().id == repo.id) {
346             item->setLevel(2);
347             updateRepoItem(item, repo);
348             return;
349         }
350     }
351 
352     // Current repo not in this group yet
353     RepoItem *item = new RepoItem(repo);
354     item->setLevel(2);
355     group->appendRow(item);
356 }
357 
checkSyncedRepo(const ServerRepo & repo)358 void RepoTreeModel::checkSyncedRepo(const ServerRepo& repo)
359 {
360     int row, n = synced_repos_category_->rowCount();
361     for (row = 0; row < n; row++) {
362         RepoItem *item = (RepoItem *)(synced_repos_category_->child(row));
363         if (item->repo().id == repo.id) {
364             updateRepoItem(item, repo);
365             return;
366         }
367     }
368 
369     // The repo is new
370     RepoItem *item = new RepoItem(repo);
371     synced_repos_category_->appendRow(item);
372 }
373 
updateRepoItem(RepoItem * item,const ServerRepo & repo)374 void RepoTreeModel::updateRepoItem(RepoItem *item, const ServerRepo& repo)
375 {
376     item->setRepo(repo);
377 }
378 
forEachRepoItem(void (RepoTreeModel::* func)(RepoItem *,void *),void * data,QStandardItem * item)379 void RepoTreeModel::forEachRepoItem(void (RepoTreeModel::*func)(RepoItem *, void *),
380                                     void *data,
381                                     QStandardItem *item)
382 {
383     if (item == nullptr) {
384         item = invisibleRootItem();
385     }
386     if (item->type() == REPO_ITEM_TYPE) {
387         (this->*func)((RepoItem *)item, data);
388     }
389 
390     int row, n = item->rowCount();
391     for (row = 0; row < n; row++) {
392         forEachRepoItem(func, data, item->child(row));
393     }
394 }
395 
forEachCategoryItem(void (* func)(RepoCategoryItem *,void *),void * data,QStandardItem * item)396 void RepoTreeModel::forEachCategoryItem(void (*func)(RepoCategoryItem *, void *),
397                                         void *data,
398                                         QStandardItem *item)
399 {
400     if (item == nullptr) {
401         item = invisibleRootItem();
402     }
403     if (item->type() == REPO_CATEGORY_TYPE) {
404         func((RepoCategoryItem *)item, data);
405     }
406 
407     int row, n = item->rowCount();
408     for (row = 0; row < n; row++) {
409         forEachCategoryItem(func, data, item->child(row));
410     }
411 }
412 
refreshLocalRepos()413 void RepoTreeModel::refreshLocalRepos()
414 {
415     if (!seafApplet->mainWindow()->isVisible()) {
416         return;
417     }
418 
419     std::vector<CloneTask> tasks;
420     seafApplet->rpcClient()->getCloneTasks(&tasks);
421 
422     forEachRepoItem(&RepoTreeModel::refreshRepoItem, (void*) &tasks);
423 }
424 
refreshRepoItem(RepoItem * item,void * data)425 void RepoTreeModel::refreshRepoItem(RepoItem *item, void *data)
426 {
427     if (!tree_view_->isExpanded(proxiedIndexFromItem(item->parent()))) {
428         return;
429     }
430 
431     if (item->syncNowClicked()) {
432         // Skip refresh repo item on which the user has clicked "sync now"
433         item->setSyncNowClicked(false);
434         return;
435     }
436 
437     LocalRepo local_repo;
438     seafApplet->rpcClient()->getLocalRepo(item->repo().id, &local_repo);
439     if (local_repo != item->localRepo()) {
440         // if (local_repo.isValid()) {
441         //     printf("local repo of %s changed\n", local_repo.name.toUtf8().data());
442         // }
443         item->setLocalRepo(local_repo);
444         QModelIndex index = indexFromItem(item);
445         emit dataChanged(index,index);
446         emit repoStatusChanged(index);
447         // printf("repo %s is changed\n", toCStr(item->repo().name));
448     }
449 
450     item->setCloneTask();
451 
452     CloneTask clone_task;
453     std::vector<CloneTask>* tasks = (std::vector<CloneTask>*)data;
454     if (!local_repo.isValid()) {
455         for (size_t i=0; i < tasks->size(); ++i) {
456             clone_task = tasks->at(i);
457             if (clone_task.repo_id == item->repo().id) {
458                 item->setCloneTask(clone_task);
459                 QModelIndex index = indexFromItem(item);
460                 emit dataChanged(index, index);
461                 emit repoStatusChanged(index);
462             }
463         }
464     }
465 }
466 
updateRepoItemAfterSyncNow(const QString & repo_id)467 void RepoTreeModel::updateRepoItemAfterSyncNow(const QString& repo_id)
468 {
469     QString id = repo_id;
470     forEachRepoItem(&RepoTreeModel::updateRepoItemAfterSyncNow, (void*) &id);
471 }
472 
updateRepoItemAfterSyncNow(RepoItem * item,void * data)473 void RepoTreeModel::updateRepoItemAfterSyncNow(RepoItem *item, void *data)
474 {
475     QString repo_id = *(QString *)data;
476     LocalRepo r = item->localRepo();
477     if (r.isValid() && r.id == repo_id) {
478         // We manually set the sync state of the repo to "SYNC_STATE_ING" to give
479         // the user immediate feedback
480 
481         r.setSyncInfo("initializing");
482         r.sync_state = LocalRepo::SYNC_STATE_ING;
483         r.sync_state_str = tr("sync initializing");
484         item->setLocalRepo(r);
485         item->setSyncNowClicked(true);
486     }
487 }
488 
onFilterTextChanged(const QString & text)489 void RepoTreeModel::onFilterTextChanged(const QString& text)
490 {
491     // Recalculate the matched repos count for each category
492     QStandardItem *root = invisibleRootItem();
493     int row, n;
494     n = root->rowCount();
495     QRegExp re = makeFilterRegExp(text);
496     for (row = 0; row < n; row++) {
497         RepoCategoryItem *category = (RepoCategoryItem *)root->child(row);
498         if (category->isGroupsRoot()) {
499             continue;
500         }
501         int j, total, matched = 0;
502         total = category->rowCount();
503         for (j = 0; j < total; j++) {
504             RepoItem *item = (RepoItem *)category->child(j);
505             if (item->repo().name.contains(re)) {
506                 matched++;
507             }
508         }
509         category->setMatchedReposCount(matched);
510     }
511 }
512 
RepoFilterProxyModel(QObject * parent)513 RepoFilterProxyModel::RepoFilterProxyModel(QObject *parent)
514     : QSortFilterProxyModel(parent),
515       has_filter_(false)
516 {
517 }
518 
setSourceModel(QAbstractItemModel * source_model)519 void RepoFilterProxyModel::setSourceModel(QAbstractItemModel *source_model)
520 {
521     QSortFilterProxyModel::setSourceModel(source_model);
522     RepoTreeModel *tree_model = (RepoTreeModel *)source_model;
523     connect(tree_model, SIGNAL(repoStatusChanged(const QModelIndex&)),
524             this, SLOT(onRepoStatusChanged(const QModelIndex&)));
525 }
526 
onRepoStatusChanged(const QModelIndex & source_index)527 void RepoFilterProxyModel::onRepoStatusChanged(const QModelIndex& source_index)
528 {
529     QModelIndex index = mapFromSource(source_index);
530     emit dataChanged(index, index);
531 }
532 
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const533 bool RepoFilterProxyModel::filterAcceptsRow(int source_row,
534                         const QModelIndex & source_parent) const
535 {
536     RepoTreeModel *tree_model = (RepoTreeModel *)(sourceModel());
537     QModelIndex index = tree_model->index(source_row, 0, source_parent);
538     QStandardItem *item = tree_model->itemFromIndex(index);
539     if (item->type() == REPO_CATEGORY_TYPE) {
540         // RepoCategoryItem *category = (RepoCategoryItem *)item;
541         // We don't filter repo categories, only filter repos by name.
542         return true;
543     } else if (item->type() == REPO_ITEM_TYPE) {
544         // Use default filtering (filter by item DisplayRole, i.e. repo name)
545         bool match = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
546         // if (match) {
547         //     RepoCategoryItem *category = (RepoCategoryItem *)(item->parent());
548         // }
549         return match;
550     }
551 
552     return false;
553 }
554 
setFilterText(const QString & text)555 void RepoFilterProxyModel::setFilterText(const QString& text)
556 {
557     has_filter_ = !text.isEmpty();
558     invalidate();
559     setFilterRegExp(makeFilterRegExp(text));
560 }
561 
562 // void RepoFilterProxyModel::sort()
563 // {
564 // }
565 
lessThan(const QModelIndex & left,const QModelIndex & right) const566 bool RepoFilterProxyModel::lessThan(const QModelIndex &left,
567                                     const QModelIndex &right) const
568 {
569     RepoTreeModel *tree_model = (RepoTreeModel *)(sourceModel());
570     QStandardItem *item_l = tree_model->itemFromIndex(left);
571     QStandardItem *item_r = tree_model->itemFromIndex(right);
572 
573     /**
574      * When we have filter: sort category by matched repos count
575      * When we have no filter: sort category by category index order
576      *
577      */
578     if (item_l->type() == REPO_CATEGORY_TYPE) {
579         // repo categories
580         RepoCategoryItem *cl = (RepoCategoryItem *)item_l;
581         RepoCategoryItem *cr = (RepoCategoryItem *)item_r;
582         if (has_filter_) {
583             // printf ("%s matched: %d, %s matched: %d\n",
584             //         cl->name().toUtf8().data(), cl->matchedReposCount(),
585             //         cr->name().toUtf8().data(), cr->matchedReposCount());
586             return cl->matchedReposCount() > cr->matchedReposCount();
587         } else {
588             int cat_l = cl->categoryIndex();
589             int cat_r = cr->categoryIndex();
590             if (cat_l == cat_r) {
591                 return cl->name() < cr->name();
592             } else {
593                 return cat_l < cat_r;
594             }
595         }
596     } else {
597         // repos
598         RepoItem *cl = (RepoItem *)item_l;
599         RepoItem *cr = (RepoItem *)item_r;
600         if (cl->repo().mtime != cr->repo().mtime) {
601             return cl->repo().mtime > cr->repo().mtime;
602         } else {
603             return cl->repo().name > cr->repo().name;
604         }
605     }
606 
607     return false;
608 }
609 
flags(const QModelIndex & index) const610 Qt::ItemFlags RepoFilterProxyModel::flags(const QModelIndex& index) const
611 {
612     Qt::ItemFlags flgs =  QSortFilterProxyModel::flags(index);
613     return flgs & ~Qt::ItemIsEditable;
614 }
615