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
***************************************************************************/
24
#include "PreCompiled.h"
27
# include <QApplication>
31
# include <QTextStream>
32
# include <QThreadPool>
35
#include <App/Application.h>
36
#include <App/Document.h>
37
#include <App/DocumentObject.h>
38
#include <Base/Console.h>
39
#include <Base/FileInfo.h>
40
#include <Base/Stream.h>
41
#include <Base/TimeInfo.h>
42
#include <Base/Tools.h>
43
#include <Base/Writer.h>
47
#include "MainWindow.h"
48
#include "ViewProvider.h"
49
#include "WaitCursor.h"
51
FC_LOG_LEVEL_INIT("App",true,true)
54
namespace sp = std::placeholders;
56
AutoSaver* AutoSaver::self = nullptr;
57
const int AutoSaveTimeout = 900000;
59
AutoSaver::AutoSaver(QObject* parent)
61
, timeout(AutoSaveTimeout)
65
App::GetApplication().signalNewDocument.connect(std::bind(&AutoSaver::slotCreateDocument, this, sp::_1));
66
App::GetApplication().signalDeleteDocument.connect(std::bind(&AutoSaver::slotDeleteDocument, this, sp::_1));
70
AutoSaver::~AutoSaver() = default;
72
AutoSaver* AutoSaver::instance()
75
self = new AutoSaver(QApplication::instance());
80
void AutoSaver::renameFile(QString dirName, QString file, QString tmpFile)
82
FC_LOG("auto saver rename " << tmpFile.toUtf8().constData()
83
<< " -> " << file.toUtf8().constData());
86
dir.rename(tmpFile,file);
89
void AutoSaver::setTimeout(int ms)
91
timeout = Base::clamp<int>(ms, 0, 3600000); // between 0 and 60 min
93
// go through the attached documents and apply the new timeout
94
for (auto & it : saverMap) {
95
if (it.second->timerId > 0)
96
killTimer(it.second->timerId);
97
int id = timeout > 0 ? startTimer(timeout) : 0;
98
it.second->timerId = id;
102
void AutoSaver::setCompressed(bool on)
104
this->compressed = on;
107
void AutoSaver::slotCreateDocument(const App::Document& Doc)
109
std::string name = Doc.getName();
110
int id = timeout > 0 ? startTimer(timeout) : 0;
111
AutoSaveProperty* as = new AutoSaveProperty(&Doc);
114
if (!this->compressed) {
115
std::string dirName = Doc.TransientDir.getValue();
116
dirName += "/fc_recovery_files";
117
Base::FileInfo fi(dirName);
118
fi.createDirectory();
119
as->dirName = dirName;
121
saverMap.insert(std::make_pair(name, as));
124
void AutoSaver::slotDeleteDocument(const App::Document& Doc)
126
std::string name = Doc.getName();
127
std::map<std::string, AutoSaveProperty*>::iterator it = saverMap.find(name);
128
if (it != saverMap.end()) {
129
if (it->second->timerId > 0)
130
killTimer(it->second->timerId);
136
void AutoSaver::saveDocument(const std::string& name, AutoSaveProperty& saver)
139
App::Document* doc = App::GetApplication().getDocument(name.c_str());
140
if (doc && !doc->testStatus(App::Document::PartialDoc)
141
&& !doc->testStatus(App::Document::TempDoc))
143
// Set the document's current transient directory
144
std::string dirName = doc->TransientDir.getValue();
145
dirName += "/fc_recovery_files";
146
saver.dirName = dirName;
148
// Write recovery meta file
149
QFile file(QString::fromLatin1("%1/fc_recovery_file.xml")
150
.arg(QString::fromUtf8(doc->TransientDir.getValue())));
151
if (file.open(QFile::WriteOnly)) {
152
QTextStream str(&file);
153
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
154
str.setCodec("UTF-8");
156
str << "<?xml version='1.0' encoding='utf-8'?>\n"
157
<< "<AutoRecovery SchemaVersion=\"1\">\n";
158
str << " <Status>Created</Status>\n";
159
str << " <Label>" << QString::fromUtf8(doc->Label.getValue()) << "</Label>\n"; // store the document's current label
160
str << " <FileName>" << QString::fromUtf8(doc->FileName.getValue()) << "</FileName>\n"; // store the document's current filename
161
str << "</AutoRecovery>\n";
165
// make sure to tmp. disable saving thumbnails because this causes trouble if the
166
// associated 3d view is not active
167
Base::Reference<ParameterGrp> hGrp = App::GetApplication().GetParameterGroupByPath
168
("User parameter:BaseApp/Preferences/Document");
169
bool save = hGrp->GetBool("SaveThumbnail",true);
170
hGrp->SetBool("SaveThumbnail",false);
172
getMainWindow()->showMessage(tr("Please wait until the AutoRecovery file has been saved..."), 5000);
173
//qApp->processEvents();
175
Base::TimeElapsed startTime;
176
// open extra scope to close ZipWriter properly
178
if (!this->compressed) {
179
RecoveryWriter writer(saver);
181
// We will be using thread pool if not compressed.
182
// So, always force binary format because ASCII
183
// is not reentrant. See PropertyPartShape::SaveDocFile
184
writer.setMode("BinaryBrep");
186
writer.putNextEntry("Document.xml");
190
// Special handling for Gui document.
191
doc->signalSaveDocument(writer);
193
// write additional files
196
// only create the file if something has changed
197
else if (!saver.touched.empty()) {
198
std::string fn = doc->TransientDir.getValue();
199
fn += "/fc_recovery_file.fcstd";
200
Base::FileInfo tmp(fn);
201
Base::ofstream file(tmp, std::ios::out | std::ios::binary);
204
Base::ZipWriter writer(file);
205
if (hGrp->GetBool("SaveBinaryBrep", true))
206
writer.setMode("BinaryBrep");
208
writer.setComment("AutoRecovery file");
209
writer.setLevel(1); // apparently the fastest compression
210
writer.putNextEntry("Document.xml");
214
// Special handling for Gui document.
215
doc->signalSaveDocument(writer);
217
// write additional files
223
Base::Console().Log("Save AutoRecovery file in %fs\n", Base::TimeElapsed::diffTimeF(startTime,Base::TimeElapsed()));
224
hGrp->SetBool("SaveThumbnail",save);
228
void AutoSaver::timerEvent(QTimerEvent * event)
230
int id = event->timerId();
231
for (auto & it : saverMap) {
232
if (it.second->timerId == id) {
234
saveDocument(it.first, *it.second);
235
it.second->touched.clear();
239
Base::Console().Error("Failed to auto-save document '%s'\n", it.first.c_str());
245
// ----------------------------------------------------------------------------
247
AutoSaveProperty::AutoSaveProperty(const App::Document* doc) : timerId(-1)
250
documentNew = const_cast<App::Document*>(doc)->signalNewObject.connect
251
(std::bind(&AutoSaveProperty::slotNewObject, this, sp::_1));
252
documentMod = const_cast<App::Document*>(doc)->signalChangedObject.connect
253
(std::bind(&AutoSaveProperty::slotChangePropertyData, this, sp::_2));
257
AutoSaveProperty::~AutoSaveProperty()
259
documentNew.disconnect();
260
documentMod.disconnect();
263
void AutoSaveProperty::slotNewObject(const App::DocumentObject& obj)
265
std::vector<App::Property*> props;
266
obj.getPropertyList(props);
268
// if an object was deleted and then restored by an undo then add all properties
269
// because this might be the data files which we may want to re-write
270
for (const auto & prop : props) {
271
slotChangePropertyData(*prop);
275
void AutoSaveProperty::slotChangePropertyData(const App::Property& prop)
277
std::stringstream str;
278
str << static_cast<const void *>(&prop) << std::ends;
279
std::string address = str.str();
280
this->touched.insert(address);
283
// ----------------------------------------------------------------------------
285
RecoveryWriter::RecoveryWriter(AutoSaveProperty& saver)
286
: Base::FileWriter(saver.dirName.c_str()), saver(saver)
290
RecoveryWriter::~RecoveryWriter() = default;
292
bool RecoveryWriter::shouldWrite(const std::string& name, const Base::Persistence *object) const
294
// Property files of a view provider can always be written because
295
// these are rather small files.
296
if (object->isDerivedFrom(App::Property::getClassTypeId())) {
297
const auto* prop = static_cast<const App::Property*>(object);
298
const App::PropertyContainer* parent = prop->getContainer();
299
if (parent && parent->isDerivedFrom(Gui::ViewProvider::getClassTypeId()))
302
else if (object->isDerivedFrom(Gui::Document::getClassTypeId())) {
306
// These are the addresses of touched properties of a document object.
307
std::stringstream str;
308
str << static_cast<const void *>(object) << std::ends;
309
std::string address = str.str();
311
// Check if the property will be exported to the same file. If the file has changed or if the property hasn't been
312
// yet exported then (re-)write the file.
313
std::map<std::string, std::string>::iterator it = saver.fileMap.find(address);
314
if (it == saver.fileMap.end() || it->second != name) {
315
saver.fileMap[address] = name;
319
std::set<std::string>::const_iterator jt = saver.touched.find(address);
320
return (jt != saver.touched.end());
325
class RecoveryRunnable : public QRunnable
328
RecoveryRunnable(const std::set<std::string>& modes, const char* dir, const char* file, const App::Property* p)
332
writer.setModes(modes);
334
dirName = QString::fromUtf8(dir);
335
fileName = QString::fromUtf8(file);
336
tmpName = QString::fromLatin1("%1.tmp%2").arg(fileName).arg(rand());
337
writer.putNextEntry(tmpName.toUtf8().constData());
339
~RecoveryRunnable() override
345
prop->SaveDocFile(writer);
348
// We could have renamed the file in this thread. However, there is
349
// still chance of crash when we deleted the original and before rename
350
// the new file. So we ask the main thread to do it. There is still
351
// possibility of crash caused by thread other than the main, but
352
// that's the best we can do for now.
353
QMetaObject::invokeMethod(AutoSaver::instance(), "renameFile",
354
Qt::QueuedConnection, Q_ARG(QString,dirName)
355
,Q_ARG(QString,fileName),Q_ARG(QString,tmpName));
360
Base::FileWriter writer;
368
void RecoveryWriter::writeFiles()
370
// use a while loop because it is possible that while
371
// processing the files new ones can be added
373
this->FileStream.close();
374
while (index < FileList.size()) {
375
FileEntry entry = FileList.begin()[index];
377
if (shouldWrite(entry.FileName, entry.Object)) {
378
std::string filePath = entry.FileName;
379
std::string::size_type pos = 0;
380
while ((pos = filePath.find('/', pos)) != std::string::npos) {
381
std::string dirName = DirName + "/" + filePath.substr(0, pos);
383
Base::FileInfo fi(dirName);
384
fi.createDirectory();
387
// For properties a copy can be created and then this can be written to disk in a thread
388
if (entry.Object->isDerivedFrom(App::Property::getClassTypeId())) {
389
const auto* prop = static_cast<const App::Property*>(entry.Object);
390
QThreadPool::globalInstance()->start(new RecoveryRunnable(getModes(), DirName.c_str(), entry.FileName.c_str(), prop));
393
std::string fileName = DirName + "/" + entry.FileName;
394
this->FileStream.open(fileName.c_str(), std::ios::out | std::ios::binary);
395
entry.Object->SaveDocFile(*this);
396
this->FileStream.close();
405
#include "moc_AutoSaver.cpp"