| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | #include <QApplication> |
| | #include <QFile> |
| | #include <QDir> |
| | #include <QRunnable> |
| | #include <QTextStream> |
| | #include <QThreadPool> |
| |
|
| | #include <App/Application.h> |
| | #include <App/Document.h> |
| | #include <App/DocumentObject.h> |
| | #include <Base/Console.h> |
| | #include <Base/FileInfo.h> |
| | #include <Base/Stream.h> |
| | #include <Base/TimeInfo.h> |
| | #include <Base/Tools.h> |
| | #include <Base/Writer.h> |
| |
|
| | #include "AutoSaver.h" |
| | #include "Document.h" |
| | #include "MainWindow.h" |
| | #include "ViewProvider.h" |
| | #include "WaitCursor.h" |
| |
|
| | FC_LOG_LEVEL_INIT("App", true, true) |
| |
|
| | using namespace Gui; |
| | namespace sp = std::placeholders; |
| |
|
| | AutoSaver* AutoSaver::self = nullptr; |
| | const int AutoSaveTimeout = 900000; |
| |
|
| | AutoSaver::AutoSaver(QObject* parent) |
| | : QObject(parent) |
| | , timeout(AutoSaveTimeout) |
| | , compressed(true) |
| | { |
| | |
| | App::GetApplication().signalNewDocument.connect( |
| | std::bind(&AutoSaver::slotCreateDocument, this, sp::_1) |
| | ); |
| | App::GetApplication().signalDeleteDocument.connect( |
| | std::bind(&AutoSaver::slotDeleteDocument, this, sp::_1) |
| | ); |
| | |
| | } |
| |
|
| | AutoSaver::~AutoSaver() = default; |
| |
|
| | AutoSaver* AutoSaver::instance() |
| | { |
| | if (!self) { |
| | self = new AutoSaver(QApplication::instance()); |
| | } |
| | return self; |
| | } |
| |
|
| | void AutoSaver::renameFile(QString dirName, QString file, QString tmpFile) |
| | { |
| | FC_LOG("auto saver rename " << tmpFile.toUtf8().constData() << " -> " << file.toUtf8().constData()); |
| | QDir dir(dirName); |
| | dir.remove(file); |
| | if (!dir.rename(tmpFile, file)) { |
| | FC_ERR( |
| | "Failed to rename autosave file " << tmpFile.toStdString() << " to " |
| | << file.toStdString() << "\n" |
| | ); |
| | } |
| | } |
| |
|
| | void AutoSaver::setTimeout(int ms) |
| | { |
| | timeout = Base::clamp<int>(ms, 0, 3600000); |
| |
|
| | |
| | for (auto& it : saverMap) { |
| | if (it.second->timerId > 0) { |
| | killTimer(it.second->timerId); |
| | } |
| | int id = timeout > 0 ? startTimer(timeout) : 0; |
| | it.second->timerId = id; |
| | } |
| | } |
| |
|
| | void AutoSaver::setCompressed(bool on) |
| | { |
| | this->compressed = on; |
| | } |
| |
|
| | void AutoSaver::slotCreateDocument(const App::Document& Doc) |
| | { |
| | std::string name = Doc.getName(); |
| | int id = timeout > 0 ? startTimer(timeout) : 0; |
| | AutoSaveProperty* as = new AutoSaveProperty(&Doc); |
| | as->timerId = id; |
| |
|
| | if (!this->compressed) { |
| | std::string dirName = Doc.TransientDir.getValue(); |
| | dirName += "/fc_recovery_files"; |
| | Base::FileInfo fi(dirName); |
| | fi.createDirectory(); |
| | as->dirName = dirName; |
| | } |
| | saverMap.insert(std::make_pair(name, as)); |
| | } |
| |
|
| | void AutoSaver::slotDeleteDocument(const App::Document& Doc) |
| | { |
| | std::string name = Doc.getName(); |
| | std::map<std::string, AutoSaveProperty*>::iterator it = saverMap.find(name); |
| | if (it != saverMap.end()) { |
| | if (it->second->timerId > 0) { |
| | killTimer(it->second->timerId); |
| | } |
| | delete it->second; |
| | saverMap.erase(it); |
| | } |
| | } |
| |
|
| | void AutoSaver::saveDocument(const std::string& name, AutoSaveProperty& saver) |
| | { |
| | Gui::WaitCursor wc; |
| | App::Document* doc = App::GetApplication().getDocument(name.c_str()); |
| | if (doc && !doc->testStatus(App::Document::PartialDoc) |
| | && !doc->testStatus(App::Document::TempDoc)) { |
| | |
| | std::string dirName = doc->TransientDir.getValue(); |
| | dirName += "/fc_recovery_files"; |
| | saver.dirName = dirName; |
| |
|
| | |
| | QFile file(QStringLiteral("%1/fc_recovery_file.xml") |
| | .arg(QString::fromUtf8(doc->TransientDir.getValue()))); |
| | if (file.open(QFile::WriteOnly)) { |
| | QTextStream str(&file); |
| | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) |
| | str.setCodec("UTF-8"); |
| | #endif |
| | str << "<?xml version='1.0' encoding='utf-8'?>\n" |
| | << "<AutoRecovery SchemaVersion=\"1\">\n"; |
| | str << " <Status>Created</Status>\n"; |
| | str << " <Label>" << QString::fromUtf8(doc->Label.getValue()) |
| | << "</Label>\n"; |
| | str << " <FileName>" << QString::fromUtf8(doc->FileName.getValue()) |
| | << "</FileName>\n"; |
| | str << "</AutoRecovery>\n"; |
| | file.close(); |
| | } |
| |
|
| | |
| | |
| | Base::Reference<ParameterGrp> hGrp = App::GetApplication().GetParameterGroupByPath( |
| | "User parameter:BaseApp/Preferences/Document" |
| | ); |
| | bool save = hGrp->GetBool("SaveThumbnail", true); |
| | hGrp->SetBool("SaveThumbnail", false); |
| |
|
| | getMainWindow()->showMessage(tr("Wait until the auto-recovery file has been saved…"), 5000); |
| | |
| |
|
| | Base::TimeElapsed startTime; |
| | |
| | { |
| | if (!this->compressed) { |
| | RecoveryWriter writer(saver); |
| |
|
| | |
| | |
| | |
| | writer.setMode("BinaryBrep"); |
| |
|
| | writer.putNextEntry("Document.xml"); |
| |
|
| | doc->Save(writer); |
| |
|
| | |
| | doc->signalSaveDocument(writer); |
| |
|
| | |
| | writer.writeFiles(); |
| | } |
| | |
| | else if (!saver.touched.empty()) { |
| | std::string fn = doc->TransientDir.getValue(); |
| | fn += "/fc_recovery_file.fcstd"; |
| | Base::FileInfo tmp(fn); |
| | Base::ofstream file(tmp, std::ios::out | std::ios::binary); |
| | if (file.is_open()) { |
| | Base::ZipWriter writer(file); |
| | if (hGrp->GetBool("SaveBinaryBrep", true)) { |
| | writer.setMode("BinaryBrep"); |
| | } |
| |
|
| | writer.setComment("AutoRecovery file"); |
| | writer.setLevel(1); |
| | writer.putNextEntry("Document.xml"); |
| |
|
| | doc->Save(writer); |
| |
|
| | |
| | doc->signalSaveDocument(writer); |
| |
|
| | |
| | writer.writeFiles(); |
| | } |
| | } |
| | } |
| |
|
| | Base::Console().log( |
| | "Save auto-recovery file in %fs\n", |
| | Base::TimeElapsed::diffTimeF(startTime, Base::TimeElapsed()) |
| | ); |
| | hGrp->SetBool("SaveThumbnail", save); |
| | } |
| | } |
| |
|
| | void AutoSaver::timerEvent(QTimerEvent* event) |
| | { |
| | int id = event->timerId(); |
| | for (auto& it : saverMap) { |
| | if (it.second->timerId == id) { |
| | try { |
| | saveDocument(it.first, *it.second); |
| | it.second->touched.clear(); |
| | break; |
| | } |
| | catch (...) { |
| | Base::Console().error("Failed to auto-save document '%s'\n", it.first.c_str()); |
| | } |
| | } |
| | } |
| | } |
| |
|
| | |
| |
|
| | AutoSaveProperty::AutoSaveProperty(const App::Document* doc) |
| | : timerId(-1) |
| | { |
| | |
| | documentNew = const_cast<App::Document*>(doc)->signalNewObject.connect( |
| | std::bind(&AutoSaveProperty::slotNewObject, this, sp::_1) |
| | ); |
| | documentMod = const_cast<App::Document*>(doc)->signalChangedObject.connect( |
| | std::bind(&AutoSaveProperty::slotChangePropertyData, this, sp::_2) |
| | ); |
| | |
| | } |
| |
|
| | AutoSaveProperty::~AutoSaveProperty() |
| | { |
| | documentNew.disconnect(); |
| | documentMod.disconnect(); |
| | } |
| |
|
| | void AutoSaveProperty::slotNewObject(const App::DocumentObject& obj) |
| | { |
| | std::vector<App::Property*> props; |
| | obj.getPropertyList(props); |
| |
|
| | |
| | |
| | for (const auto& prop : props) { |
| | slotChangePropertyData(*prop); |
| | } |
| | } |
| |
|
| | void AutoSaveProperty::slotChangePropertyData(const App::Property& prop) |
| | { |
| | std::stringstream str; |
| | str << static_cast<const void*>(&prop) << std::ends; |
| | std::string address = str.str(); |
| | this->touched.insert(address); |
| | } |
| |
|
| | |
| |
|
| | RecoveryWriter::RecoveryWriter(AutoSaveProperty& saver) |
| | : Base::FileWriter(saver.dirName.c_str()) |
| | , saver(saver) |
| | {} |
| |
|
| | RecoveryWriter::~RecoveryWriter() = default; |
| |
|
| | bool RecoveryWriter::shouldWrite(const std::string& name, const Base::Persistence* object) const |
| | { |
| | |
| | |
| | if (object->isDerivedFrom<App::Property>()) { |
| | const auto* prop = static_cast<const App::Property*>(object); |
| | const App::PropertyContainer* parent = prop->getContainer(); |
| | if (parent && parent->isDerivedFrom<Gui::ViewProvider>()) { |
| | return true; |
| | } |
| | } |
| | else if (object->isDerivedFrom<Gui::Document>()) { |
| | return true; |
| | } |
| |
|
| | |
| | std::stringstream str; |
| | str << static_cast<const void*>(object) << std::ends; |
| | std::string address = str.str(); |
| |
|
| | |
| | |
| | std::map<std::string, std::string>::iterator it = saver.fileMap.find(address); |
| | if (it == saver.fileMap.end() || it->second != name) { |
| | saver.fileMap[address] = name; |
| | return true; |
| | } |
| |
|
| | std::set<std::string>::const_iterator jt = saver.touched.find(address); |
| | return (jt != saver.touched.end()); |
| | } |
| |
|
| | namespace Gui |
| | { |
| |
|
| | class RecoveryRunnable: public QRunnable |
| | { |
| | public: |
| | RecoveryRunnable( |
| | const std::set<std::string>& modes, |
| | const char* dir, |
| | const char* file, |
| | const App::Property* p |
| | ) |
| | : prop(p->Copy()) |
| | , writer(dir) |
| | { |
| | writer.setModes(modes); |
| |
|
| | dirName = QString::fromUtf8(dir); |
| | fileName = QString::fromUtf8(file); |
| | tmpName = QStringLiteral("%1.tmp%2").arg(fileName).arg(rand()); |
| | writer.putNextEntry(tmpName.toUtf8().constData()); |
| | } |
| | ~RecoveryRunnable() override |
| | { |
| | delete prop; |
| | } |
| | void run() override |
| | { |
| | try { |
| | prop->SaveDocFile(writer); |
| | writer.close(); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | QMetaObject::invokeMethod( |
| | AutoSaver::instance(), |
| | "renameFile", |
| | Qt::QueuedConnection, |
| | Q_ARG(QString, dirName), |
| | Q_ARG(QString, fileName), |
| | Q_ARG(QString, tmpName) |
| | ); |
| | } |
| | catch (const Base::Exception& e) { |
| | Base::Console().warning("Exception in auto-saving: %s\n", e.what()); |
| | } |
| | catch (const std::exception& e) { |
| | Base::Console().warning("C++ exception in auto-saving: %s\n", e.what()); |
| | } |
| | catch (...) { |
| | Base::Console().warning("Unknown exception in auto-saving\n"); |
| | } |
| | } |
| |
|
| | private: |
| | App::Property* prop; |
| | Base::FileWriter writer; |
| | QString dirName; |
| | QString fileName; |
| | QString tmpName; |
| | }; |
| |
|
| | } |
| |
|
| | void RecoveryWriter::writeFiles() |
| | { |
| | |
| | |
| | size_t index = 0; |
| | this->FileStream.close(); |
| | while (index < FileList.size()) { |
| | FileEntry entry = FileList.begin()[index]; |
| |
|
| | if (shouldWrite(entry.FileName, entry.Object)) { |
| | std::string filePath = entry.FileName; |
| | std::string::size_type pos = 0; |
| | while ((pos = filePath.find('/', pos)) != std::string::npos) { |
| | std::string dirName = DirName + "/" + filePath.substr(0, pos); |
| | pos++; |
| | Base::FileInfo fi(dirName); |
| | fi.createDirectory(); |
| | } |
| |
|
| | |
| | if (entry.Object->isDerivedFrom<App::Property>()) { |
| | const auto* prop = static_cast<const App::Property*>(entry.Object); |
| | QThreadPool::globalInstance()->start( |
| | new RecoveryRunnable(getModes(), DirName.c_str(), entry.FileName.c_str(), prop) |
| | ); |
| | } |
| | else { |
| | std::string fileName = DirName + "/" + entry.FileName; |
| | this->FileStream.open(fileName.c_str(), std::ios::out | std::ios::binary); |
| | entry.Object->SaveDocFile(*this); |
| | this->FileStream.close(); |
| | } |
| | } |
| |
|
| | index++; |
| | } |
| | } |
| |
|
| |
|
| | #include "moc_AutoSaver.cpp" |
| |
|