Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_safe_document_loader.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2013 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <QTimer>
10#include <QFileSystemWatcher>
11#include <QRandomGenerator>
12#include <QApplication>
13#include <QFileInfo>
14#include <QDir>
15#include <QUrl>
16
17#include <KoStore.h>
18#include <QTemporaryFile>
19
20#include <kis_paint_layer.h>
21#include <kis_group_layer.h>
22#include "KisDocument.h"
23#include <kis_image.h>
25#include "KisPart.h"
26#include "KisUsageLogger.h"
27
28#include <kis_layer_utils.h>
29#include <kis_global.h>
30
31class FileSystemWatcherWrapper : public QObject
32{
33 Q_OBJECT
34
35private:
41
42 struct FileEntry
43 {
45 QElapsedTimer lostTimer;
47 };
48
49public:
51 : m_reattachmentCompressor(100, KisSignalCompressor::FIRST_INACTIVE),
52 m_lostCompressor(1000, KisSignalCompressor::FIRST_INACTIVE)
53
54 {
55 connect(&m_watcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged(QString)));
56 connect(&m_reattachmentCompressor, SIGNAL(timeout()), SLOT(slotReattachFiles()));
57 connect(&m_lostCompressor, SIGNAL(timeout()), SLOT(slotFindLostFiles()));
58 }
59
60 bool addPath(const QString &file) {
61 bool result = true;
62 const QString ufile = unifyFilePath(file);
63
64 if (m_fileEntries.contains(ufile)) {
65 m_fileEntries[ufile].numConnections++;
66 } else {
67 m_fileEntries.insert(ufile, {1, {}, Exists});
68 result = m_watcher.addPath(ufile);
69 }
70
71 return result;
72 }
73
74 bool removePath(const QString &file) {
75 bool result = true;
76 const QString ufile = unifyFilePath(file);
77
79
80 if (m_fileEntries[ufile].numConnections == 1) {
81 m_fileEntries.remove(ufile);
82 result = m_watcher.removePath(ufile);
83 } else {
84 m_fileEntries[ufile].numConnections--;
85 }
86 return result;
87 }
88
90 return m_watcher.files();
91 }
92
93private Q_SLOTS:
94 void slotFileChanged(const QString &path) {
95
97
98 FileEntry &entry = m_fileEntries[path];
99
100 // re-add the file after QSaveFile optimization
101 if (!m_watcher.files().contains(path)) {
102
103 if (QFileInfo(path).exists()) {
104 const FileState oldState = entry.state;
105
106 m_watcher.addPath(path);
107 entry.state = Exists;
108
109 if (oldState == Lost) {
110 Q_EMIT fileExistsStateChanged(path, true);
111 } else {
112 Q_EMIT fileChanged(path);
113 }
114
115 } else {
116
117 if (entry.state == Exists) {
118 entry.state = Reattaching;
119 entry.lostTimer.start();
121
122 } else if (entry.state == Reattaching) {
123 if (entry.lostTimer.elapsed() > 10000) {
124 entry.state = Lost;
126 Q_EMIT fileExistsStateChanged(path, false);
127 } else {
129 }
130
131 } else if (entry.state == Lost) {
133 }
134
135
136#if 0
137 const bool shouldSpitWarning =
138 absenceTimeMSec <= 600000 &&
139 ((absenceTimeMSec >= 60000 && (absenceTimeMSec % 60000 == 0)) ||
140 (absenceTimeMSec >= 10000 && (absenceTimeMSec % 10000 == 0)));
141
142 if (shouldSpitWarning) {
143 QString message;
144 QTextStream log(&message);
146
147 log << "WARNING: couldn't reconnect to a removed file layer's file (" << path << "). File is not available for " << absenceTimeMSec / 1000 << " seconds";
148
149 qWarning() << message;
150 KisUsageLogger::log(message);
151
152 if (absenceTimeMSec == 600000) {
153 message.clear();
154 log.reset();
155
156 log << "Giving up... :( No more reports about " << path;
157
158 qWarning() << message;
159 KisUsageLogger::log(message);
160 }
161 }
162#endif
163 }
164 } else {
165 Q_EMIT fileChanged(path);
166 }
167 }
168
170 for (auto it = m_fileEntries.constBegin(); it != m_fileEntries.constEnd(); ++it) {
171 if (it.value().state == Lost)
172 slotFileChanged(it.key());
173 }
174 }
175
177 for (auto it = m_fileEntries.constBegin(); it != m_fileEntries.constEnd(); ++it) {
178 if (it.value().state == Reattaching)
179 slotFileChanged(it.key());
180 }
181 }
182
183
184Q_SIGNALS:
185 void fileChanged(const QString &path);
186 void fileExistsStateChanged(const QString &path, bool exists);
187
188public:
189 static QString unifyFilePath(const QString &path) {
190 return QFileInfo(path).absoluteFilePath();
191 }
192
193private:
194 QFileSystemWatcher m_watcher;
195 QHash<QString, int> m_pathCount;
198 QHash<QString, int> m_lostFilesAbsenceCounter;
199 QHash<QString, FileEntry> m_fileEntries;
200};
201
202Q_GLOBAL_STATIC(FileSystemWatcherWrapper, s_fileSystemWatcher)
203
204
206{
208 : fileChangedSignalCompressor(500 /* ms */, KisSignalCompressor::POSTPONE)
209 {
210 }
211
212 QScopedPointer<KisDocument> doc;
214 bool isLoading = false;
215 bool fileChangedFlag = false;
216 QString path;
218
219 qint64 initialFileSize {0};
221
222 int failureCount {0};
223};
224
225KisSafeDocumentLoader::KisSafeDocumentLoader(const QString &path, QObject *parent)
226 : QObject(parent),
227 m_d(new Private())
228{
229 connect(s_fileSystemWatcher, SIGNAL(fileChanged(QString)),
230 SLOT(fileChanged(QString)));
231
232 connect(s_fileSystemWatcher, SIGNAL(fileExistsStateChanged(QString, bool)),
233 SLOT(slotFileExistsStateChanged(QString, bool)));
234
235 connect(&m_d->fileChangedSignalCompressor, SIGNAL(timeout()),
236 SLOT(fileChangedCompressed()));
237
238 setPath(path);
239}
240
242{
243 if (!m_d->path.isEmpty()) {
244 s_fileSystemWatcher->removePath(m_d->path);
245 }
246
247 delete m_d;
248}
249
250void KisSafeDocumentLoader::setPath(const QString &path)
251{
252 if (path.isEmpty()) return;
253
254 if (!m_d->path.isEmpty()) {
255 s_fileSystemWatcher->removePath(m_d->path);
256 }
257
258 m_d->path = path;
259 s_fileSystemWatcher->addPath(m_d->path);
260}
261
266
274
275void KisSafeDocumentLoader::slotFileExistsStateChanged(const QString &path, bool fileExists)
276{
278 Q_EMIT fileExistsStateChanged(fileExists);
279 if (fileExists) {
280 fileChanged(path);
281 }
282 }
283}
284
286{
287 if (m_d->isLoading) return;
288
289 QFileInfo initialFileInfo(m_d->path);
290 m_d->initialFileSize = initialFileInfo.size();
291 m_d->initialFileTimeStamp = initialFileInfo.lastModified();
292
293 // it may happen when the file is flushed by
294 // so other application
295 if (!m_d->initialFileSize) return;
296
297 m_d->isLoading = true;
298 m_d->fileChangedFlag = false;
299
301 QDir::tempPath() + '/' +
302 QString("krita_file_layer_copy_%1_%2.%3")
303 .arg(QApplication::applicationPid())
304 .arg(QRandomGenerator::global()->generate())
305 .arg(initialFileInfo.suffix());
306
307 QFile::copy(m_d->path, m_d->temporaryPath);
308
309
310 if (!sync) {
311 QTimer::singleShot(100, Qt::CoarseTimer, this, SLOT(delayedLoadStart()));
312 } else {
313 QApplication::processEvents();
315 }
316}
317
319{
320 QFileInfo originalInfo(m_d->path);
321 QFileInfo tempInfo(m_d->temporaryPath);
322 bool successfullyLoaded = false;
323
324 if (!m_d->fileChangedFlag &&
325 originalInfo.size() == m_d->initialFileSize &&
326 originalInfo.lastModified() == m_d->initialFileTimeStamp &&
327 tempInfo.size() == m_d->initialFileSize) {
328
329 m_d->doc.reset(KisPart::instance()->createTemporaryDocument());
330 m_d->doc->setFileBatchMode(true);
331
332 auto loadPathNatively = [&doc = m_d->doc](const QString &path) -> bool {
333 const bool successfullyLoaded = doc->openPath(path, KisDocument::DontAddToRecent);
334 if (successfullyLoaded) {
335 // Wait for required updates, if any. BUG: 448256
336 KisLayerUtils::forceAllDelayedNodesUpdate(doc->image()->root());
337 doc->image()->waitForDone();
338 }
339 return successfullyLoaded;
340 };
341
342 if (m_d->path.toLower().endsWith("ora") || m_d->path.toLower().endsWith("kra")) {
343 QScopedPointer<KoStore> store(KoStore::createStore(m_d->temporaryPath, KoStore::Read));
344 if (store && !store->bad()) {
345 if (store->open(QString("mergedimage.png"))) {
356 QTemporaryFile temporaryFile(QDir::tempPath() + QLatin1String("/krita_merged_image_XXXXXX.png"));
357 temporaryFile.open();
358
359 QByteArray buffer(BUFSIZ, 0);
360 qint64 totalWritten = 0;
361 const qint64 expectedFileSize = store->size();
362
363 while (true) {
364 qint64 read = store->read(buffer.data(), buffer.size());
365 if (read < 0) {
366 warnKrita << "Failed to read from mergedimage.png for the file layer's projection";
367 break;
368 } else if (read == 0) {
369 // End of file
370 break;
371 } else {
372 // Successful read, try to write it.
373 qint64 written = temporaryFile.write(buffer.constData(), read);
374 if (written < 0) {
375 // Write error.
376 warnKrita << "Failed to wirte mergedimage.png into a temporary file for the file layer's projection"
377 << temporaryFile.fileName() << ":" << temporaryFile.error();
378 break;
379 }
380 // We may not have written as much as we read, but we handle
381 // that at the end.
382 totalWritten += written;
383 }
384 }
385
386 temporaryFile.close();
387 store->close();
388
389 if (totalWritten == expectedFileSize) {
390 successfullyLoaded = loadPathNatively(temporaryFile.fileName());
391 } else {
392 successfullyLoaded = false;
393 }
394 }
395 else {
396 qWarning() << "delayedLoadStart: Could not open mergedimage.png";
397 }
398 }
399 else {
400 qWarning() << "delayedLoadStart: Store was bad";
401 }
402 }
403 else {
404 successfullyLoaded = loadPathNatively(m_d->temporaryPath);
405 }
406 } else {
407 dbgKrita << "File was modified externally. Restarting.";
411 dbgKrita << ppVar(originalInfo.size());
412 dbgKrita << ppVar(originalInfo.lastModified());
413 dbgKrita << ppVar(tempInfo.size());
414 }
415
416 QFile::remove(m_d->temporaryPath);
417 m_d->isLoading = false;
418
419 if (!successfullyLoaded) {
420 // Restart the attempt
421 m_d->failureCount++;
422 if (m_d->failureCount >= 3) {
423 Q_EMIT loadingFailed();
424 }
425 else {
427 }
428 }
429 else {
430 KisPaintDeviceSP paintDevice = new KisPaintDevice(m_d->doc->image()->colorSpace());
431 KisPaintDeviceSP projection = m_d->doc->image()->projection();
432 paintDevice->makeCloneFrom(projection, projection->extent());
433 Q_EMIT loadingFinished(paintDevice,
434 m_d->doc->image()->xRes(),
435 m_d->doc->image()->yRes(),
436 m_d->doc->image()->size());
437 }
438
439 m_d->doc.reset();
440}
441
442#include "kis_safe_document_loader.moc"
Q_GLOBAL_STATIC(KisStoragePluginRegistry, s_instance)
static QString unifyFilePath(const QString &path)
bool addPath(const QString &file)
QHash< QString, FileEntry > m_fileEntries
KisSignalCompressor m_reattachmentCompressor
void slotFileChanged(const QString &path)
void fileExistsStateChanged(const QString &path, bool exists)
bool removePath(const QString &file)
void fileChanged(const QString &path)
QHash< QString, int > m_lostFilesAbsenceCounter
QRect extent() const
void makeCloneFrom(KisPaintDeviceSP src, const QRect &rect)
static KisPart * instance()
Definition KisPart.cpp:131
void fileChangedCompressed(bool sync=false)
void fileExistsStateChanged(bool fileExists)
void setPath(const QString &path)
void loadingFinished(KisPaintDeviceSP paintDevice, qreal xRes, qreal yRes, const QSize &size)
void slotFileExistsStateChanged(const QString &path, bool fileExists)
KisSafeDocumentLoader(const QString &path="", QObject *parent=0)
static void log(const QString &message)
Logs with date/time.
@ Read
Definition KoStore.h:29
static KoStore * createStore(const QString &fileName, Mode mode, const QByteArray &appIdentification=QByteArray(), Backend backend=Auto, bool writeMimetype=true)
Definition KoStore.cpp:39
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define dbgKrita
Definition kis_debug.h:45
#define warnKrita
Definition kis_debug.h:87
#define ppVar(var)
Definition kis_debug.h:155
void forceAllDelayedNodesUpdate(KisNodeSP root)
void setUtf8OnStream(QTextStream &stream)