1
/***************************************************************************
2
* Copyright (c) 2015 Werner Mayer <wmayer[at]users.sourceforge.net> *
4
* This file is part of the FreeCAD CAx development system. *
6
* This library is free software; you can redistribute it and/or *
7
* modify it under the terms of the GNU Library General Public *
8
* License as published by the Free Software Foundation; either *
9
* version 2 of the License, or (at your option) any later version. *
11
* This library is distributed in the hope that it will be useful, *
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
* GNU Library General Public License for more details. *
16
* You should have received a copy of the GNU Library General Public *
17
* License along with this library; see the file COPYING.LIB. If not, *
18
* write to the Free Software Foundation, Inc., 59 Temple Place, *
19
* Suite 330, Boston, MA 02111-1307, USA *
21
***************************************************************************/
23
// Implement FileWriter which puts files into a directory
24
// write a property to file only when it has been modified
25
// implement xml meta file
27
#include "PreCompiled.h"
30
# include <boost/interprocess/sync/file_lock.hpp>
31
# include <QApplication>
32
# include <QCloseEvent>
36
# include <QDomDocument>
38
# include <QHeaderView>
42
# include <QMessageBox>
44
# include <QTextStream>
45
# include <QTreeWidgetItem>
50
#include <App/Application.h>
51
#include <App/Document.h>
52
#include <Base/Exception.h>
53
#include <Gui/Application.h>
54
#include <Gui/Command.h>
55
#include <Gui/DlgCheckableMessageBox.h>
56
#include <Gui/Document.h>
57
#include <Gui/MainWindow.h>
59
#include "DocumentRecovery.h"
60
#include "ui_DocumentRecovery.h"
61
#include "WaitCursor.h"
64
FC_LOG_LEVEL_INIT("Gui", true, true)
67
using namespace Gui::Dialog;
68
namespace sp = std::placeholders;
70
// taken from the script doctools.py
71
std::string DocumentRecovery::doctools =
72
"import os,sys,string\n"
74
"import xml.sax.handler\n"
75
"import xml.sax.xmlreader\n"
78
"# SAX handler to parse the Document.xml\n"
79
"class DocumentHandler(xml.sax.handler.ContentHandler):\n"
80
" def __init__(self, dirname):\n"
82
" self.dirname = dirname\n"
84
" def startElement(self, name, attributes):\n"
85
" if name == 'XLink':\n"
87
" item=attributes.get(\"file\")\n"
89
" self.files.append(os.path.join(self.dirname,str(item)))\n"
91
" def characters(self, data):\n"
94
" def endElement(self, name):\n"
97
"def extractDocument(filename, outpath):\n"
98
" zfile=zipfile.ZipFile(filename)\n"
99
" files=zfile.namelist()\n"
102
" data=zfile.read(i)\n"
103
" dirs=i.split(\"/\")\n"
104
" if len(dirs) > 1:\n"
108
" curpath=curpath+\"/\"+j\n"
109
" os.mkdir(curpath)\n"
110
" output=open(outpath+\"/\"+i,\'wb\')\n"
111
" output.write(data)\n"
114
"def createDocument(filename, outpath):\n"
115
" files=getFilesList(filename)\n"
116
" dirname=os.path.dirname(filename)\n"
117
" guixml=os.path.join(dirname,\"GuiDocument.xml\")\n"
118
" if os.path.exists(guixml):\n"
119
" files.extend(getFilesList(guixml))\n"
120
" compress=zipfile.ZipFile(outpath,\'w\',zipfile.ZIP_DEFLATED)\n"
122
" dirs=os.path.split(i)\n"
123
" #print i, dirs[-1]\n"
124
" compress.write(i,dirs[-1],zipfile.ZIP_DEFLATED)\n"
127
"def getFilesList(filename):\n"
128
" dirname=os.path.dirname(filename)\n"
129
" handler=DocumentHandler(dirname)\n"
130
" parser=xml.sax.make_parser()\n"
131
" parser.setContentHandler(handler)\n"
132
" parser.parse(filename)\n"
135
" files.append(filename)\n"
136
" files.extend(iter(handler.files))\n"
141
namespace Gui { namespace Dialog {
142
class DocumentRecoveryPrivate
145
using XmlConfig = QMap<QString, QString>;
148
Unknown = 0, /*!< The file is not available */
149
Created = 1, /*!< The file was created but not processed so far*/
150
Overage = 2, /*!< The recovery file is older than the actual project file */
151
Success = 3, /*!< The file could be recovered */
152
Failure = 4, /*!< The file could not be recovered */
160
Status status = Unknown;
162
Ui_DocumentRecovery ui;
164
QList<Info> recoveryInfo;
166
Info getRecoveryInfo(const QFileInfo&) const;
167
void writeRecoveryInfo(const Info&) const;
168
XmlConfig readXmlFile(const QString& fn) const;
174
DocumentRecovery::DocumentRecovery(const QList<QFileInfo>& dirs, QWidget* parent)
175
: QDialog(parent), d_ptr(new DocumentRecoveryPrivate())
177
d_ptr->ui.setupUi(this);
178
connect(d_ptr->ui.buttonCleanup, &QPushButton::clicked,
179
this, &DocumentRecovery::onButtonCleanupClicked);
180
d_ptr->ui.buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Start Recovery"));
181
d_ptr->ui.treeWidget->header()->setSectionResizeMode(QHeaderView::Stretch);
183
d_ptr->recovered = false;
185
for (QList<QFileInfo>::const_iterator it = dirs.begin(); it != dirs.end(); ++it) {
186
DocumentRecoveryPrivate::Info info = d_ptr->getRecoveryInfo(*it);
188
if (info.status == DocumentRecoveryPrivate::Created) {
189
d_ptr->recoveryInfo << info;
191
auto item = new QTreeWidgetItem(d_ptr->ui.treeWidget);
192
item->setText(0, info.label);
193
item->setToolTip(0, info.tooltip);
194
item->setText(1, tr("Not yet recovered"));
195
item->setToolTip(1, info.projectFile);
196
d_ptr->ui.treeWidget->addTopLevelItem(item);
203
DocumentRecovery::~DocumentRecovery() = default;
205
bool DocumentRecovery::foundDocuments() const
207
Q_D(const DocumentRecovery);
208
return (!d->recoveryInfo.isEmpty());
211
QString DocumentRecovery::createProjectFile(const QString& documentXml)
213
QString source = documentXml;
214
QFileInfo fi(source);
215
QString dest = fi.dir().absoluteFilePath(QString::fromLatin1("fc_recovery_file.fcstd"));
217
std::stringstream str;
218
str << doctools << "\n";
219
str << "createDocument(\"" << (const char*)source.toUtf8()
220
<< "\", \"" << (const char*)dest.toUtf8() << "\")";
221
Gui::Command::runCommand(Gui::Command::App, str.str().c_str());
226
void DocumentRecovery::closeEvent(QCloseEvent* e)
228
// Do not disable the X button in the title bar
229
// #0004281: Close Document Recovery
233
void DocumentRecovery::accept()
235
Q_D(DocumentRecovery);
241
std::vector<int> indices;
242
std::vector<std::string> filenames, paths, labels, errs;
243
for (auto &info : d->recoveryInfo) {
245
QTreeWidgetItem* item = d_ptr->ui.treeWidget->topLevelItem(index);
248
QString file = info.projectFile;
250
if (fi.fileName() == QLatin1String("Document.xml"))
251
file = createProjectFile(info.projectFile);
253
paths.emplace_back(file.toUtf8().constData());
254
filenames.emplace_back(info.fileName.toUtf8().constData());
255
labels.emplace_back(info.label.toUtf8().constData());
256
indices.push_back(index);
259
catch (const std::exception& e) {
260
errorInfo = QString::fromLatin1(e.what());
262
catch (const Base::Exception& e) {
263
errorInfo = QString::fromLatin1(e.what());
266
errorInfo = tr("Unknown problem occurred");
269
if (!errorInfo.isEmpty()) {
270
info.status = DocumentRecoveryPrivate::Failure;
272
item->setText(1, tr("Failed to recover"));
273
item->setToolTip(1, errorInfo);
274
item->setForeground(1, QColor(170,0,0));
276
d->writeRecoveryInfo(info);
280
auto docs = App::GetApplication().openDocuments(filenames,&paths,&labels,&errs);
282
for (size_t i = 0; i < docs.size(); ++i) {
283
auto &info = d->recoveryInfo[indices[i]];
284
QTreeWidgetItem* item = d_ptr->ui.treeWidget->topLevelItem(indices[i]);
285
if (!docs[i] || !errs[i].empty()) {
287
App::GetApplication().closeDocument(docs[i]->getName());
288
info.status = DocumentRecoveryPrivate::Failure;
291
item->setText(1, tr("Failed to recover"));
292
item->setToolTip(1, QString::fromUtf8(errs[i].c_str()));
293
item->setForeground(1, QColor(170,0,0));
295
// write back current status
296
d->writeRecoveryInfo(info);
299
auto gdoc = Application::Instance->getDocument(docs[i]);
301
gdoc->setModified(true);
303
info.status = DocumentRecoveryPrivate::Success;
305
item->setText(1, tr("Successfully recovered"));
306
item->setForeground(1, QColor(0,170,0));
309
QDir transDir(QString::fromUtf8(docs[i]->TransientDir.getValue()));
311
QFileInfo xfi(info.xmlFile);
312
QFileInfo fi(info.projectFile);
315
if (fi.fileName() == QLatin1String("fc_recovery_file.fcstd")) {
316
transDir.remove(fi.fileName());
317
res = transDir.rename(fi.absoluteFilePath(),fi.fileName());
320
transDir.rmdir(fi.dir().dirName());
321
res = transDir.rename(fi.absolutePath(),fi.dir().dirName());
325
transDir.remove(xfi.fileName());
326
res = transDir.rename(xfi.absoluteFilePath(),xfi.fileName());
330
FC_WARN("Failed to move recovery file of document '"
331
<< docs[i]->Label.getValue() << "'");
334
DocumentRecoveryCleaner().clearDirectory(QFileInfo(xfi.absolutePath()));
335
QDir().rmdir(xfi.absolutePath());
338
// DO NOT write success into recovery info, in case the program
339
// crash again before the user save the just recovered file.
343
d->ui.buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Finish"));
344
d->ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
352
void DocumentRecoveryPrivate::writeRecoveryInfo(const DocumentRecoveryPrivate::Info& info) const
354
// Write recovery meta file
355
QFile file(info.xmlFile);
356
if (file.open(QFile::WriteOnly)) {
357
QTextStream str(&file);
358
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
359
str.setCodec("UTF-8");
361
str << "<?xml version='1.0' encoding='utf-8'?>\n"
362
<< "<AutoRecovery SchemaVersion=\"1\">\n";
363
switch (info.status) {
365
str << " <Status>Created</Status>\n";
368
str << " <Status>Deprecated</Status>\n";
371
str << " <Status>Success</Status>\n";
374
str << " <Status>Failure</Status>\n";
377
str << " <Status>Unknown</Status>\n";
380
str << " <Label>" << info.label << "</Label>\n";
381
str << " <FileName>" << info.fileName << "</FileName>\n";
382
str << "</AutoRecovery>\n";
387
DocumentRecoveryPrivate::Info DocumentRecoveryPrivate::getRecoveryInfo(const QFileInfo& fi) const
389
DocumentRecoveryPrivate::Info info;
390
info.status = DocumentRecoveryPrivate::Unknown;
391
info.label = qApp->translate("StdCmdNew","Unnamed");
394
QDir doc_dir(fi.absoluteFilePath());
395
QDir rec_dir(doc_dir.absoluteFilePath(QLatin1String("fc_recovery_files")));
397
// compressed recovery file
398
if (doc_dir.exists(QLatin1String("fc_recovery_file.fcstd"))) {
399
file = doc_dir.absoluteFilePath(QLatin1String("fc_recovery_file.fcstd"));
401
// separate files for recovery
402
else if (rec_dir.exists(QLatin1String("Document.xml"))) {
403
file = rec_dir.absoluteFilePath(QLatin1String("Document.xml"));
406
info.status = DocumentRecoveryPrivate::Created;
407
info.projectFile = file;
408
info.tooltip = fi.fileName();
410
// when the Xml meta exists get some relevant information
411
info.xmlFile = doc_dir.absoluteFilePath(QLatin1String("fc_recovery_file.xml"));
412
if (doc_dir.exists(QLatin1String("fc_recovery_file.xml"))) {
413
XmlConfig cfg = readXmlFile(info.xmlFile);
415
if (cfg.contains(QString::fromLatin1("Label"))) {
416
info.label = cfg[QString::fromLatin1("Label")];
419
if (cfg.contains(QString::fromLatin1("FileName"))) {
420
info.fileName = cfg[QString::fromLatin1("FileName")];
423
if (cfg.contains(QString::fromLatin1("Status"))) {
424
QString status = cfg[QString::fromLatin1("Status")];
425
if (status == QLatin1String("Deprecated"))
426
info.status = DocumentRecoveryPrivate::Overage;
427
else if (status == QLatin1String("Success"))
428
info.status = DocumentRecoveryPrivate::Success;
429
else if (status == QLatin1String("Failure"))
430
info.status = DocumentRecoveryPrivate::Failure;
433
if (info.status == DocumentRecoveryPrivate::Created) {
434
// compare the modification dates
435
QFileInfo fileInfo(info.fileName);
436
if (!info.fileName.isEmpty() && fileInfo.exists()) {
437
QDateTime dateRecv = QFileInfo(file).lastModified();
438
QDateTime dateProj = fileInfo.lastModified();
439
if (dateRecv < dateProj) {
440
info.status = DocumentRecoveryPrivate::Overage;
441
writeRecoveryInfo(info);
442
qWarning() << "Ignore recovery file " << file.toUtf8()
443
<< " because it is older than the project file" << info.fileName.toUtf8() << "\n";
452
DocumentRecoveryPrivate::XmlConfig DocumentRecoveryPrivate::readXmlFile(const QString& fn) const
454
DocumentRecoveryPrivate::XmlConfig cfg;
455
QDomDocument domDocument;
457
if (!file.open(QFile::ReadOnly))
464
if (!domDocument.setContent(&file, true, &errorStr, &errorLine,
469
QDomElement root = domDocument.documentElement();
470
if (root.tagName() != QLatin1String("AutoRecovery")) {
476
QVector<QString> filter;
477
filter << QString::fromLatin1("Label");
478
filter << QString::fromLatin1("FileName");
479
filter << QString::fromLatin1("Status");
482
if (!root.isNull()) {
483
child = root.firstChildElement();
484
while (!child.isNull()) {
485
QString name = child.localName();
486
QString value = child.text();
487
if (std::find(filter.begin(), filter.end(), name) != filter.end())
489
child = child.nextSiblingElement();
496
void DocumentRecovery::contextMenuEvent(QContextMenuEvent* ev)
498
QList<QTreeWidgetItem*> items = d_ptr->ui.treeWidget->selectedItems();
499
if (!items.isEmpty()) {
501
menu.addAction(tr("Delete"), this, &DocumentRecovery::onDeleteSection);
502
menu.exec(ev->globalPos());
506
void DocumentRecovery::onDeleteSection()
508
QMessageBox msgBox(this);
509
msgBox.setIcon(QMessageBox::Warning);
510
msgBox.setWindowTitle(tr("Cleanup"));
511
msgBox.setText(tr("Are you sure you want to delete the selected transient directories?"));
512
msgBox.setInformativeText(tr("When deleting the selected transient directory you won't be able to recover any files afterwards."));
513
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
514
msgBox.setDefaultButton(QMessageBox::No);
515
int ret = msgBox.exec();
516
if (ret == QMessageBox::No)
519
QList<QTreeWidgetItem*> items = d_ptr->ui.treeWidget->selectedItems();
520
QDir tmp = QString::fromUtf8(App::Application::getUserCachePath().c_str());
521
for (QList<QTreeWidgetItem*>::iterator it = items.begin(); it != items.end(); ++it) {
522
int index = d_ptr->ui.treeWidget->indexOfTopLevelItem(*it);
523
QTreeWidgetItem* item = d_ptr->ui.treeWidget->takeTopLevelItem(index);
525
QString projectFile = item->toolTip(0);
526
DocumentRecoveryCleaner().clearDirectory(QFileInfo(tmp.filePath(projectFile)));
527
tmp.rmdir(projectFile);
531
int numItems = d_ptr->ui.treeWidget->topLevelItemCount();
533
d_ptr->ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
534
d_ptr->ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true);
538
void DocumentRecovery::onButtonCleanupClicked()
540
QMessageBox msgBox(this);
541
msgBox.setIcon(QMessageBox::Warning);
542
msgBox.setWindowTitle(tr("Cleanup"));
543
msgBox.setText(tr("Are you sure you want to delete all transient directories?"));
544
msgBox.setInformativeText(tr("When deleting all transient directories you won't be able to recover any files afterwards."));
545
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
546
msgBox.setDefaultButton(QMessageBox::No);
547
int ret = msgBox.exec();
548
if (ret == QMessageBox::No)
551
d_ptr->ui.treeWidget->clear();
552
d_ptr->ui.buttonCleanup->setEnabled(false);
553
d_ptr->ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
554
d_ptr->ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true);
556
DocumentRecoveryHandler handler;
557
handler.checkForPreviousCrashes(std::bind(&DocumentRecovery::cleanup, this, sp::_1, sp::_2, sp::_3));
558
DlgCheckableMessageBox::showMessage(tr("Delete"), tr("Transient directories deleted."));
562
void DocumentRecovery::cleanup(QDir& tmp, const QList<QFileInfo>& dirs, const QString& lockFile)
564
if (!dirs.isEmpty()) {
565
for (QList<QFileInfo>::const_iterator jt = dirs.cbegin(); jt != dirs.cend(); ++jt) {
566
DocumentRecoveryCleaner().clearDirectory(*jt);
567
tmp.rmdir(jt->fileName());
570
tmp.remove(lockFile);
573
// ----------------------------------------------------------------------------
575
bool DocumentRecoveryFinder::checkForPreviousCrashes()
578
DocumentRecoveryHandler handler;
579
handler.checkForPreviousCrashes(std::bind(&DocumentRecoveryFinder::checkDocumentDirs, this, sp::_1, sp::_2, sp::_3));
582
return showRecoveryDialogIfNeeded();
585
void DocumentRecoveryFinder::checkDocumentDirs(QDir& tmp, const QList<QFileInfo>& dirs, const QString& fn)
587
if (dirs.isEmpty()) {
588
// delete the lock file immediately if no transient directories are related
592
int countDeletedDocs = 0;
593
QString recovery_files = QString::fromLatin1("fc_recovery_files");
594
for (QList<QFileInfo>::const_iterator it = dirs.cbegin(); it != dirs.cend(); ++it) {
595
QDir doc_dir(it->absoluteFilePath());
596
doc_dir.setFilter(QDir::NoDotAndDotDot|QDir::AllEntries);
597
uint entries = doc_dir.entryList().count();
599
// in this case we can delete the transient directory because
600
// we cannot do anything
601
if (tmp.rmdir(it->filePath()))
604
// search for the existence of a recovery file
605
else if (doc_dir.exists(QLatin1String("fc_recovery_file.xml"))) {
606
// store the transient directory in case it's not empty
607
restoreDocFiles << *it;
609
// search for the 'fc_recovery_files' sub-directory and check that it's the only entry
610
else if (entries == 1 && doc_dir.exists(recovery_files)) {
611
// if the sub-directory is empty delete the transient directory
612
QDir rec_dir(doc_dir.absoluteFilePath(recovery_files));
613
rec_dir.setFilter(QDir::NoDotAndDotDot|QDir::AllEntries);
614
if (rec_dir.entryList().isEmpty()) {
615
doc_dir.rmdir(recovery_files);
616
if (tmp.rmdir(it->filePath()))
622
// all directories corresponding to the lock file have been deleted
623
// so delete the lock file, too
624
if (countDeletedDocs == dirs.size()) {
630
bool DocumentRecoveryFinder::showRecoveryDialogIfNeeded()
632
bool foundRecoveryFiles = false;
633
if (!restoreDocFiles.isEmpty()) {
634
Gui::Dialog::DocumentRecovery dlg(restoreDocFiles, Gui::getMainWindow());
635
if (dlg.foundDocuments()) {
636
foundRecoveryFiles = true;
641
return foundRecoveryFiles;
644
// ----------------------------------------------------------------------------
646
void DocumentRecoveryHandler::checkForPreviousCrashes(const std::function<void(QDir&, const QList<QFileInfo>&, const QString&)> & callableFunc) const
648
QDir tmp = QString::fromUtf8(App::Application::getUserCachePath().c_str());
649
tmp.setNameFilters(QStringList() << QString::fromLatin1("*.lock"));
650
tmp.setFilter(QDir::Files);
652
QString exeName = QString::fromStdString(App::Application::getExecutableName());
653
QList<QFileInfo> locks = tmp.entryInfoList();
654
for (QList<QFileInfo>::iterator it = locks.begin(); it != locks.end(); ++it) {
655
QString bn = it->baseName();
656
// ignore the lock file for this instance
657
QString pid = QString::number(QCoreApplication::applicationPid());
658
if (bn.startsWith(exeName) && bn.indexOf(pid) < 0) {
659
QString fn = it->absoluteFilePath();
661
#if !defined(FC_OS_WIN32) || (BOOST_VERSION < 107600)
662
boost::interprocess::file_lock flock(fn.toUtf8());
664
boost::interprocess::file_lock flock(fn.toStdWString().c_str());
666
if (flock.try_lock()) {
667
// OK, this file is a leftover from a previous crash
668
QString crashed_pid = bn.mid(exeName.length()+1);
669
// search for transient directories with this PID
671
QTextStream str(&filter);
672
str << exeName << "_Doc_*_" << crashed_pid;
673
tmp.setNameFilters(QStringList() << filter);
674
tmp.setFilter(QDir::Dirs);
675
QList<QFileInfo> dirs = tmp.entryInfoList();
677
callableFunc(tmp, dirs, it->fileName());
683
// ----------------------------------------------------------------------------
685
void DocumentRecoveryCleaner::clearDirectory(const QFileInfo& dir)
687
QDir qThisDir(dir.absoluteFilePath());
688
if (!qThisDir.exists())
691
// Remove all files in this directory
692
qThisDir.setFilter(QDir::Files);
693
QStringList files = qThisDir.entryList();
694
subtractFiles(files);
695
for (QStringList::iterator it = files.begin(); it != files.end(); ++it) {
697
qThisDir.remove(file);
700
// Clear this directory of any sub-directories
701
qThisDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
702
QFileInfoList subdirs = qThisDir.entryInfoList();
703
subtractDirs(subdirs);
704
for (QFileInfoList::iterator it = subdirs.begin(); it != subdirs.end(); ++it) {
706
qThisDir.rmdir(it->fileName());
710
void DocumentRecoveryCleaner::subtractFiles(QStringList& files)
712
if (!ignoreFiles.isEmpty() && !files.isEmpty()) {
713
#if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
714
auto set1 = QSet<QString>(files.begin(), files.end());
715
auto set2 = QSet<QString>(ignoreFiles.begin(), ignoreFiles.end());
717
files = QList<QString>(set1.begin(), set1.end());
719
QSet<QString> set1 = files.toSet();
720
QSet<QString> set2 = ignoreFiles.toSet();
722
files = set1.toList();
727
void DocumentRecoveryCleaner::subtractDirs(QFileInfoList& dirs)
729
if (!ignoreDirs.isEmpty() && !dirs.isEmpty()) {
730
for (const auto& it : std::as_const(ignoreDirs)) {
736
void DocumentRecoveryCleaner::setIgnoreFiles(const QStringList& list)
741
void DocumentRecoveryCleaner::setIgnoreDirectories(const QFileInfoList& list)
746
#include "moc_DocumentRecovery.cpp"