1 /*
2    Bacula(R) - The Network Backup Solution
3 
4    Copyright (C) 2000-2020 Kern Sibbald
5 
6    The original author of Bacula is Kern Sibbald, with contributions
7    from many others, a complete list can be found in the file AUTHORS.
8 
9    You may use this file and others of this release according to the
10    license defined in the LICENSE file, which includes the Affero General
11    Public License, v3.0 ("AGPLv3") and some additional permissions and
12    terms pursuant to its AGPLv3 Section 7.
13 
14    This notice must be preserved when any source code is
15    conveyed and/or propagated.
16 
17    Bacula(R) is a registered trademark of Kern Sibbald.
18 */
19 /*
20  * Restore Wizard: File selection page
21  *
22  * Written by Norbert Bizet, May MMXVII
23  *
24  */
25 #include "common.h"
26 #include "fileselectwizardpage.h"
27 #include "filesmodel.h"
28 #include "ui_fileselectwizardpage.h"
29 #include "restorewizard.h"
30 #include "task.h"
31 #include <QStandardItemModel>
32 #include <QShortcut>
33 
FileSelectWizardPage(QWidget * parent)34 FileSelectWizardPage::FileSelectWizardPage(QWidget *parent) :
35     QWizardPage(parent),
36     m_currentSourceId(0),
37     m_currentPathStr(""),
38     ui(new Ui::FileSelectWizardPage),
39     src_files_model(new FileSourceModel),
40     dest_files_model(new FileDestModel),
41     m_filterTimer(new QTimer),
42     res(NULL),
43     need_optimize(true)
44 {
45     ui->setupUi(this);
46 
47     /* keep track of the current source view selection */
48     registerField("currentSourceId", this, "currentSourceId", "currentSourceIdChanged");
49     registerField("currentPathStr", this, "currentPathStr", "currentPathStrChanged");
50     registerField("fileFilter", ui->FilterEdit);
51     /* usefull to the following pages */
52     registerField("jobIds", this, "jobIds", "jobIdsChanged");
53     registerField("fileIds", this, "fileIds", "fileIdsChanged");
54     registerField("dirIds", this, "dirIds", "dirIdsChanged");
55     registerField("hardlinks", this, "hardlinks", "hardlinksChanged");
56     registerField("pluginIds", this, "pluginIds", "pluginIdsChanged");
57     registerField("pluginNames", this, "pluginNames", "pluginNamesChanged");
58 
59     QStringList headers;
60     headers << tr("Name") << tr("Size") << tr("Date");
61 
62     ui->sourceTableView->setModel(src_files_model);
63     //ui->sourceTableView->setLayoutMode(QListView::Batched);
64     //ui->sourceTableView->setBatchSize(BATCH_SIZE);
65     src_files_model->setHorizontalHeaderLabels(headers);
66     connect(ui->sourceTableView, SIGNAL(activated(const QModelIndex &)), this, SLOT(changeCurrentFolder(const QModelIndex&)));
67     connect(ui->sourceTableView, SIGNAL(activated(const QModelIndex &)), this, SLOT(updateSourceModel()));
68 
69     ui->destTableView->setModel(dest_files_model);
70     dest_files_model->setHorizontalHeaderLabels(headers);
71     connect(dest_files_model, SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SIGNAL(completeChanged()));
72     connect(dest_files_model, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SIGNAL(completeChanged()));
73 
74     connect(ui->filePathComboPath, SIGNAL(activated(const QString &)), this, SLOT(changeCurrentText(const QString &)));
75     connect(ui->filePathComboPath, SIGNAL(activated(const QString &)), this, SLOT(updateSourceModel()));
76 
77     connect(ui->FilterEdit, SIGNAL(textChanged(const QString &)), this, SLOT(delayedFilter()));
78 
79     m_filterTimer->setSingleShot(true);
80     connect(m_filterTimer, SIGNAL(timeout()), this, SLOT(unFreezeSrcView()));
81     connect(m_filterTimer, SIGNAL(timeout()), this, SLOT(updateSourceModel()));
82 
83     QShortcut *shortcut = new QShortcut(QKeySequence(Qt::Key_Delete), this);
84     QObject::connect(shortcut, SIGNAL(activated()), this, SLOT(deleteDestSelection()));
85 }
86 
~FileSelectWizardPage()87 FileSelectWizardPage::~FileSelectWizardPage()
88 {
89     delete ui;
90 }
91 
initializePage()92 void FileSelectWizardPage::initializePage()
93 {
94     /* first request synchronous */
95     if (res) {
96         task t;
97         t.init(res, -1);
98 
99         const char *p = field("fileFilter").toString().toLatin1().data();
100         POOL_MEM info2;
101         pm_strcpy(info2, p);
102         t.arg2 = info2.c_str();
103 
104         p = currentPathStr().toLatin1().data();
105         POOL_MEM info3;
106         pm_strcpy(info3, p);
107         t.arg3 = info3.c_str();
108 
109         t.model = src_files_model;
110 
111         t.get_job_files(field("currentJob").toString().toLatin1().data(), currentSourceId());
112     }
113 
114     need_optimize = true;
115 }
116 
isComplete() const117 bool FileSelectWizardPage::isComplete() const
118 {
119     return dest_files_model->rowCount() != 0;
120 }
121 
nextId() const122 int FileSelectWizardPage::nextId() const
123 {
124     /* if some plugins are used in current selection, move to plugin page, otherwise options */
125     if (field("pluginIds").toString().isEmpty()) {
126         return RestoreWizard::RW_ADVANCEDOPTIONS_PAGE;
127     }
128     return RestoreWizard::RW_PLUGIN_PAGE;
129 }
130 
validatePage()131 bool FileSelectWizardPage::validatePage()
132 {
133     /* compute result fields based on destination model content */
134     QStringList fileids, jobids, dirids, findexes;
135     struct stat statp;
136     int32_t LinkFI;
137 
138     for (int row=0; row < dest_files_model->rowCount(); ++row) {
139         QModelIndex idx = dest_files_model->index(row, 0);
140         if (idx.data(TypeRole) == TYPEROLE_FILE) {
141             fileids << idx.data(FileIdRole).toString();
142             jobids << idx.data(JobIdRole).toString();
143             decode_stat(idx.data(LStatRole).toString().toLocal8Bit().data(),
144                         &statp, sizeof(statp), &LinkFI);
145             if (LinkFI) {
146                 findexes << idx.data(JobIdRole).toString() + "," + QString().setNum(LinkFI);
147             }
148         } else /* TYPEROLE_DIRECTORY */
149         {
150             dirids << idx.data(PathIdRole).toString();
151             jobids << idx.data(JobIdRole).toString().split(","); /* Can have multiple jobids */
152         }
153     }
154 
155     fileids.removeDuplicates();
156     jobids.removeDuplicates();
157     dirids.removeDuplicates();
158     findexes.removeDuplicates();
159 
160     m_jobIds = jobids.join(",");
161     m_fileIds = fileids.join(",");
162     m_dirIds = dirids.join(",");
163     m_hardlinks = findexes.join(",");
164 
165     /* plugin Ids and Names are retrieved now, so NextId() can decide to schedule the PluginPage before RestoreOptionsPage */
166     /* Stored as properties so thru the next Wizard Page can use them */
167     task t;
168     t.init(res, -1); /* pass res, ID is set to -1 because task method is called synchronously */
169     m_pluginIds = t.plugins_ids(m_jobIds);
170     m_pluginNames = t.plugins_names(m_jobIds);
171 
172     return true;
173 }
174 
updateSourceModel()175 void FileSelectWizardPage::updateSourceModel()
176 {
177     /* subsequent request async */
178     if (res) {
179         task *t = new task();
180 
181         /* optimize size only once */
182         if (need_optimize) {
183             connect(t, SIGNAL(done(task*)), this, SLOT(optimizeSize()));
184             need_optimize = false;
185         }
186         connect(t, SIGNAL(done(task*)), t, SLOT(deleteLater()));
187 
188         t->init(res, TASK_LIST_JOB_FILES);
189 
190         const char *p = field("currentJob").toString().toLatin1().data();
191         POOL_MEM info;
192         pm_strcpy(info, p);
193         t->arg = info.c_str();
194 
195         p = field("fileFilter").toString().toLatin1().data();
196         POOL_MEM info2;
197         pm_strcpy(info2, p);
198         t->arg2 = info2.c_str();
199 
200         p = currentPathStr().toLatin1().data();
201         POOL_MEM info3;
202         pm_strcpy(info3, p);
203         t->arg3 = info3.c_str();
204 
205         t->pathId = currentSourceId();
206         t->model = src_files_model;
207 
208         res->wrk->queue(t);
209     }
210 }
211 
optimizeSize()212 void FileSelectWizardPage::optimizeSize()
213 {
214     int w = ui->destTableView->width()/4;
215     ui->destTableView->horizontalHeader()->resizeSection( 0, w*2);
216     ui->destTableView->horizontalHeader()->resizeSection( 1, w);
217     ui->destTableView->horizontalHeader()->resizeSection( 2, w );
218     ui->destTableView->horizontalHeader()->setStretchLastSection(true);
219 
220     w = ui->sourceTableView->width()/4;
221     ui->sourceTableView->horizontalHeader()->resizeSection( 0, w*2);
222     ui->sourceTableView->horizontalHeader()->resizeSection( 1, w);
223     ui->sourceTableView->horizontalHeader()->resizeSection( 2, w );
224     ui->sourceTableView->horizontalHeader()->setStretchLastSection(true);
225 }
226 
changeCurrentFolder(const QModelIndex & current)227 void FileSelectWizardPage::changeCurrentFolder(const QModelIndex& current)
228 {
229     if (current.isValid()) {
230         QStandardItem *item = src_files_model->itemFromIndex(current);
231         if (item && item->data(TypeRole) == TYPEROLE_DIRECTORY) {
232             QString path = item->text();
233             m_currentSourceId = item->data(PathIdRole).toULongLong();
234             if (m_currentSourceId  > 0) {
235                 // Choose .. update current path to parent dir
236                 if (path == "..") {
237                     if (m_currentPathStr == "/") {
238                         m_currentPathStr = "";
239                     } else {
240                         m_currentPathStr.remove(QRegExp("[^/]+/$"));
241                     }
242 
243                 } else if (path == "/" && m_currentPathStr == "") {
244                     m_currentPathStr += path;
245 
246                 } else if (path != "/") {
247                     m_currentPathStr += path;
248                 }
249             }
250 
251             emit currentSourceIdChanged();
252             emit currentPathStrChanged();
253 
254             int idx = ui->filePathComboPath->findText(m_currentPathStr);
255             if (idx >= 0) {
256                 ui->filePathComboPath->setCurrentIndex(idx);
257             } else {
258                 ui->filePathComboPath->insertItem(-1, m_currentPathStr, QVariant(m_currentSourceId));
259                 ui->filePathComboPath->model()->sort(0);
260                 ui->filePathComboPath->setCurrentIndex(ui->filePathComboPath->findText(m_currentPathStr));
261             }
262         }
263     }
264 }
265 
changeCurrentText(const QString & current)266 void FileSelectWizardPage::changeCurrentText(const QString& current)
267 {
268     int idx = ui->filePathComboPath->findText(current);
269     m_currentSourceId = ui->filePathComboPath->itemData(idx).toULongLong();
270     m_currentPathStr = ui->filePathComboPath->itemText(idx);
271     emit currentSourceIdChanged();
272 }
273 
deleteDestSelection()274 void FileSelectWizardPage::deleteDestSelection()
275 {
276     QMap<int, int> rows;
277     foreach (QModelIndex index, ui->destTableView->selectionModel()->selectedIndexes())
278         rows.insert(index.row(), 0);
279     QMapIterator<int, int> r(rows);
280     r.toBack();
281     while (r.hasPrevious()) {
282         r.previous();
283         ui->destTableView->model()->removeRow(r.key());
284     }
285 }
286 
delayedFilter()287 void FileSelectWizardPage::delayedFilter()
288 {
289     freezeSrcView();
290     m_filterTimer->start( 100 );
291 }
292 
freezeSrcView()293 void FileSelectWizardPage::freezeSrcView()
294 {
295     ui->sourceTableView->setUpdatesEnabled(false);
296 }
297 
unFreezeSrcView()298 void FileSelectWizardPage::unFreezeSrcView()
299 {
300     ui->sourceTableView->setUpdatesEnabled(true);
301     ui->sourceTableView->update();
302 }
303