/*************************************************************************** * Copyright (c) 2015 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ // Implement FileWriter which puts files into a directory // write a property to file only when it has been modified // implement xml meta file #include "PreCompiled.h" #ifndef _PreComp_ # include # include # include # include # include # include # include # include # include # include # include # include # include # include # include # include # include #endif #include "DocumentRecovery.h" #include "ui_DocumentRecovery.h" #include "WaitCursor.h" #include #include #include #include #include #include #include #include using namespace Gui; using namespace Gui::Dialog; // taken from the script doctools.py std::string DocumentRecovery::doctools = "import os,sys,string\n" "import xml.sax\n" "import xml.sax.handler\n" "import xml.sax.xmlreader\n" "import zipfile\n" "\n" "# SAX handler to parse the Document.xml\n" "class DocumentHandler(xml.sax.handler.ContentHandler):\n" " def __init__(self, dirname):\n" " self.files = []\n" " self.dirname = dirname\n" "\n" " def startElement(self, name, attributes):\n" " item=attributes.get(\"file\")\n" " if item != None:\n" " self.files.append(os.path.join(self.dirname,str(item)))\n" "\n" " def characters(self, data):\n" " return\n" "\n" " def endElement(self, name):\n" " return\n" "\n" "def extractDocument(filename, outpath):\n" " zfile=zipfile.ZipFile(filename)\n" " files=zfile.namelist()\n" "\n" " for i in files:\n" " data=zfile.read(i)\n" " dirs=i.split(\"/\")\n" " if len(dirs) > 1:\n" " dirs.pop()\n" " curpath=outpath\n" " for j in dirs:\n" " curpath=curpath+\"/\"+j\n" " os.mkdir(curpath)\n" " output=open(outpath+\"/\"+i,\'wb\')\n" " output.write(data)\n" " output.close()\n" "\n" "def createDocument(filename, outpath):\n" " files=getFilesList(filename)\n" " dirname=os.path.dirname(filename)\n" " guixml=os.path.join(dirname,\"GuiDocument.xml\")\n" " if os.path.exists(guixml):\n" " files.extend(getFilesList(guixml))\n" " compress=zipfile.ZipFile(outpath,\'w\',zipfile.ZIP_DEFLATED)\n" " for i in files:\n" " dirs=os.path.split(i)\n" " #print i, dirs[-1]\n" " compress.write(i,dirs[-1],zipfile.ZIP_DEFLATED)\n" " compress.close()\n" "\n" "def getFilesList(filename):\n" " dirname=os.path.dirname(filename)\n" " handler=DocumentHandler(dirname)\n" " parser=xml.sax.make_parser()\n" " parser.setContentHandler(handler)\n" " parser.parse(filename)\n" "\n" " files=[]\n" " files.append(filename)\n" " files.extend(iter(handler.files))\n" " return files\n" ; namespace Gui { namespace Dialog { class DocumentRecoveryPrivate { public: typedef QMap XmlConfig; enum Status { Unknown = 0, /*!< The file is not available */ Created = 1, /*!< The file was created but not processed so far*/ Overage = 2, /*!< The recovery file is older than the actual project file */ Success = 3, /*!< The file could be recovered */ Failure = 4, /*!< The file could not be recovered */ }; struct Info { QString projectFile; QString xmlFile; QString label; QString fileName; QString tooltip; Status status; }; Ui_DocumentRecovery ui; bool recovered; QList recoveryInfo; Info getRecoveryInfo(const QFileInfo&) const; void writeRecoveryInfo(const Info&) const; XmlConfig readXmlFile(const QString& fn) const; }; } } DocumentRecovery::DocumentRecovery(const QList& dirs, QWidget* parent) : QDialog(parent), d_ptr(new DocumentRecoveryPrivate()) { d_ptr->ui.setupUi(this); d_ptr->ui.buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Start Recovery")); #if QT_VERSION >= 0x050000 d_ptr->ui.treeWidget->header()->setSectionResizeMode(QHeaderView::Stretch); #else d_ptr->ui.treeWidget->header()->setResizeMode(QHeaderView::Stretch); #endif d_ptr->recovered = false; for (QList::const_iterator it = dirs.begin(); it != dirs.end(); ++it) { DocumentRecoveryPrivate::Info info = d_ptr->getRecoveryInfo(*it); if (info.status == DocumentRecoveryPrivate::Created) { d_ptr->recoveryInfo << info; QTreeWidgetItem* item = new QTreeWidgetItem(d_ptr->ui.treeWidget); item->setText(0, info.label); item->setToolTip(0, info.tooltip); item->setText(1, tr("Not yet recovered")); item->setToolTip(1, info.projectFile); d_ptr->ui.treeWidget->addTopLevelItem(item); } } } DocumentRecovery::~DocumentRecovery() { } bool DocumentRecovery::foundDocuments() const { Q_D(const DocumentRecovery); return (!d->recoveryInfo.isEmpty()); } QString DocumentRecovery::createProjectFile(const QString& documentXml) { QString source = documentXml; QFileInfo fi(source); QString dest = fi.dir().absoluteFilePath(QString::fromLatin1("fc_recovery_file.fcstd")); std::stringstream str; str << doctools << "\n"; str << "createDocument(\"" << (const char*)source.toUtf8() << "\", \"" << (const char*)dest.toUtf8() << "\")"; Gui::Command::runCommand(Gui::Command::App, str.str().c_str()); return dest; } void DocumentRecovery::closeEvent(QCloseEvent* e) { Q_D(DocumentRecovery); if (!d->recoveryInfo.isEmpty()) e->ignore(); } void DocumentRecovery::accept() { Q_D(DocumentRecovery); if (!d->recovered) { WaitCursor wc; int index = 0; for (QList::iterator it = d->recoveryInfo.begin(); it != d->recoveryInfo.end(); ++it, index++) { std::string documentName; QString errorInfo; QTreeWidgetItem* item = d_ptr->ui.treeWidget->topLevelItem(index); try { QString file = it->projectFile; QFileInfo fi(file); if (fi.fileName() == QLatin1String("Document.xml")) file = createProjectFile(it->projectFile); App::Document* document = App::GetApplication().newDocument(); documentName = document->getName(); document->FileName.setValue(file.toUtf8().constData()); // If something goes wrong an exception will be thrown here document->restore(); file = it->fileName; document->FileName.setValue(file.toUtf8().constData()); document->Label.setValue(it->label.toUtf8().constData()); // Set the modified flag so that the user cannot close by accident Gui::Document* guidoc = Gui::Application::Instance->getDocument(document); if (guidoc) { guidoc->setModified(true); } } catch (const std::exception& e) { errorInfo = QString::fromLatin1(e.what()); } catch (const Base::Exception& e) { errorInfo = QString::fromLatin1(e.what()); } catch (...) { errorInfo = tr("Unknown problem occurred"); } // an error occurred so close the document again if (!errorInfo.isEmpty()) { if (!documentName.empty()) App::GetApplication().closeDocument(documentName.c_str()); it->status = DocumentRecoveryPrivate::Failure; if (item) { item->setText(1, tr("Failed to recover")); item->setToolTip(1, errorInfo); item->setForeground(1, QColor(170,0,0)); } } // everything OK else { it->status = DocumentRecoveryPrivate::Success; if (item) { item->setText(1, tr("Successfully recovered")); item->setForeground(1, QColor(0,170,0)); } } // write back current status d->writeRecoveryInfo(*it); } d->ui.buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Finish")); d->ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); d->recovered = true; } else { QDialog::accept(); } } void DocumentRecoveryPrivate::writeRecoveryInfo(const DocumentRecoveryPrivate::Info& info) const { // Write recovery meta file QFile file(info.xmlFile); if (file.open(QFile::WriteOnly)) { QTextStream str(&file); str.setCodec("UTF-8"); str << "" << endl << "" << endl; switch (info.status) { case Created: str << " Created" << endl; break; case Overage: str << " Deprecated" << endl; break; case Success: str << " Success" << endl; break; case Failure: str << " Failure" << endl; break; default: str << " Unknown" << endl; break; } str << " " << endl; str << " " << info.fileName << "" << endl; str << "" << endl; file.close(); } } DocumentRecoveryPrivate::Info DocumentRecoveryPrivate::getRecoveryInfo(const QFileInfo& fi) const { DocumentRecoveryPrivate::Info info; info.status = DocumentRecoveryPrivate::Unknown; info.label = qApp->translate("StdCmdNew","Unnamed"); QString file; QDir doc_dir(fi.absoluteFilePath()); QDir rec_dir(doc_dir.absoluteFilePath(QLatin1String("fc_recovery_files"))); // compressed recovery file if (doc_dir.exists(QLatin1String("fc_recovery_file.fcstd"))) { file = doc_dir.absoluteFilePath(QLatin1String("fc_recovery_file.fcstd")); } // separate files for recovery else if (rec_dir.exists(QLatin1String("Document.xml"))) { file = rec_dir.absoluteFilePath(QLatin1String("Document.xml")); } info.status = DocumentRecoveryPrivate::Created; info.projectFile = file; info.tooltip = fi.fileName(); // when the Xml meta exists get some relevant information info.xmlFile = doc_dir.absoluteFilePath(QLatin1String("fc_recovery_file.xml")); if (doc_dir.exists(QLatin1String("fc_recovery_file.xml"))) { XmlConfig cfg = readXmlFile(info.xmlFile); if (cfg.contains(QString::fromLatin1("Label"))) { info.label = cfg[QString::fromLatin1("Label")]; } if (cfg.contains(QString::fromLatin1("FileName"))) { info.fileName = cfg[QString::fromLatin1("FileName")]; } if (cfg.contains(QString::fromLatin1("Status"))) { QString status = cfg[QString::fromLatin1("Status")]; if (status == QLatin1String("Deprecated")) info.status = DocumentRecoveryPrivate::Overage; else if (status == QLatin1String("Success")) info.status = DocumentRecoveryPrivate::Success; else if (status == QLatin1String("Failure")) info.status = DocumentRecoveryPrivate::Failure; } if (info.status == DocumentRecoveryPrivate::Created) { // compare the modification dates QFileInfo fileInfo(info.fileName); if (!info.fileName.isEmpty() && fileInfo.exists()) { QDateTime dateRecv = QFileInfo(file).lastModified(); QDateTime dateProj = fileInfo.lastModified(); if (dateRecv < dateProj) { info.status = DocumentRecoveryPrivate::Overage; writeRecoveryInfo(info); qWarning() << "Ignore recovery file " << file.toUtf8() << " because it is older than the project file" << info.fileName.toUtf8() << "\n"; } } } } return info; } DocumentRecoveryPrivate::XmlConfig DocumentRecoveryPrivate::readXmlFile(const QString& fn) const { DocumentRecoveryPrivate::XmlConfig cfg; QDomDocument domDocument; QFile file(fn); if (!file.open(QFile::ReadOnly)) return cfg; QString errorStr; int errorLine; int errorColumn; if (!domDocument.setContent(&file, true, &errorStr, &errorLine, &errorColumn)) { return cfg; } QDomElement root = domDocument.documentElement(); if (root.tagName() != QLatin1String("AutoRecovery")) { return cfg; } file.close(); QVector filter; filter << QString::fromLatin1("Label"); filter << QString::fromLatin1("FileName"); filter << QString::fromLatin1("Status"); QDomElement child; if (!root.isNull()) { child = root.firstChildElement(); while (!child.isNull()) { QString name = child.localName(); QString value = child.text(); if (std::find(filter.begin(), filter.end(), name) != filter.end()) cfg[name] = value; child = child.nextSiblingElement(); } } return cfg; } void DocumentRecovery::contextMenuEvent(QContextMenuEvent* ev) { QList items = d_ptr->ui.treeWidget->selectedItems(); if (!items.isEmpty()) { QMenu menu; menu.addAction(tr("Delete"), this, SLOT(onDeleteSection())); menu.exec(ev->globalPos()); } } void DocumentRecovery::onDeleteSection() { QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Warning); msgBox.setWindowTitle(tr("Cleanup")); msgBox.setText(tr("Are you sure you want to delete the selected transient directories?")); msgBox.setInformativeText(tr("When deleting the selected transient directory you won't be able to recover any files afterwards.")); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret == QMessageBox::No) return; QList items = d_ptr->ui.treeWidget->selectedItems(); QDir tmp = QString::fromUtf8(App::Application::getTempPath().c_str()); for (QList::iterator it = items.begin(); it != items.end(); ++it) { int index = d_ptr->ui.treeWidget->indexOfTopLevelItem(*it); QTreeWidgetItem* item = d_ptr->ui.treeWidget->takeTopLevelItem(index); QString projectFile = item->toolTip(0); clearDirectory(QFileInfo(tmp.filePath(projectFile))); tmp.rmdir(projectFile); delete item; } int numItems = d_ptr->ui.treeWidget->topLevelItemCount(); if (numItems == 0) { d_ptr->ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); d_ptr->ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); } } void DocumentRecovery::on_buttonCleanup_clicked() { QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Warning); msgBox.setWindowTitle(tr("Cleanup")); msgBox.setText(tr("Are you sure you want to delete all transient directories?")); msgBox.setInformativeText(tr("When deleting all transient directory you won't be able to recover any files afterwards.")); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret == QMessageBox::No) return; d_ptr->ui.treeWidget->clear(); d_ptr->ui.buttonCleanup->setEnabled(false); d_ptr->ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); d_ptr->ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); QDir tmp = QString::fromUtf8(App::Application::getTempPath().c_str()); tmp.setNameFilters(QStringList() << QString::fromLatin1("*.lock")); tmp.setFilter(QDir::Files); QList restoreDocFiles; QString exeName = QString::fromLatin1(App::GetApplication().getExecutableName()); QList locks = tmp.entryInfoList(); for (QList::iterator it = locks.begin(); it != locks.end(); ++it) { QString bn = it->baseName(); // ignore the lock file for this instance QString pid = QString::number(QCoreApplication::applicationPid()); if (bn.startsWith(exeName) && bn.indexOf(pid) < 0) { QString fn = it->absoluteFilePath(); boost::interprocess::file_lock flock((const char*)fn.toLocal8Bit()); if (flock.try_lock()) { // OK, this file is a leftover from a previous crash QString crashed_pid = bn.mid(exeName.length()+1); // search for transient directories with this PID QString filter; QTextStream str(&filter); str << exeName << "_Doc_*_" << crashed_pid; tmp.setNameFilters(QStringList() << filter); tmp.setFilter(QDir::Dirs); QList dirs = tmp.entryInfoList(); if (!dirs.isEmpty()) { for (QList::iterator jt = dirs.begin(); jt != dirs.end(); ++jt) { clearDirectory(*jt); tmp.rmdir(jt->fileName()); } } tmp.remove(it->fileName()); } } } QMessageBox::information(this, tr("Finished"), tr("Transient directories deleted.")); } void DocumentRecovery::clearDirectory(const QFileInfo& dir) { QDir qThisDir(dir.absoluteFilePath()); if (!qThisDir.exists()) return; // Remove all files in this directory qThisDir.setFilter(QDir::Files); QStringList files = qThisDir.entryList(); for (QStringList::iterator it = files.begin(); it != files.end(); ++it) { QString file = *it; qThisDir.remove(file); } // Clear this directory of any sub-directories qThisDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList subdirs = qThisDir.entryInfoList(); for (QFileInfoList::iterator it = subdirs.begin(); it != subdirs.end(); ++it) { clearDirectory(*it); qThisDir.rmdir(it->fileName()); } } #include "moc_DocumentRecovery.cpp"