File indexing completed on 2024-05-12 16:01:41

0001 /*
0002  *  SPDX-FileCopyrightText: 2013 Dmitry Kazakov <dimula73@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kis_safe_document_loader.h"
0008 
0009 #include <QTimer>
0010 #include <QFileSystemWatcher>
0011 #include <QApplication>
0012 #include <QFileInfo>
0013 #include <QDir>
0014 #include <QUrl>
0015 
0016 #include <KoStore.h>
0017 
0018 #include <kis_paint_layer.h>
0019 #include <kis_group_layer.h>
0020 #include "KisDocument.h"
0021 #include <kis_image.h>
0022 #include "kis_signal_compressor.h"
0023 #include "KisPart.h"
0024 #include "KisUsageLogger.h"
0025 
0026 #include <kis_layer_utils.h>
0027 
0028 class FileSystemWatcherWrapper : public QObject
0029 {
0030     Q_OBJECT
0031 
0032 private:
0033     enum FileState {
0034         Exists = 0,
0035         Reattaching,
0036         Lost
0037     };
0038 
0039     struct FileEntry
0040     {
0041         int numConnections = 0;
0042         QElapsedTimer lostTimer;
0043         FileState state = Exists;
0044     };
0045 
0046 public:
0047     FileSystemWatcherWrapper()
0048         : m_reattachmentCompressor(100, KisSignalCompressor::FIRST_INACTIVE),
0049           m_lostCompressor(1000, KisSignalCompressor::FIRST_INACTIVE)
0050 
0051     {
0052         connect(&m_watcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged(QString)));
0053         connect(&m_reattachmentCompressor, SIGNAL(timeout()), SLOT(slotReattachFiles()));
0054         connect(&m_lostCompressor, SIGNAL(timeout()), SLOT(slotFindLostFiles()));
0055     }
0056 
0057     bool addPath(const QString &file) {
0058         bool result = true;
0059         const QString ufile = unifyFilePath(file);
0060 
0061         if (m_fileEntries.contains(ufile)) {
0062             m_fileEntries[ufile].numConnections++;
0063         } else {
0064             m_fileEntries.insert(ufile, {1, {}, Exists});
0065             result = m_watcher.addPath(ufile);
0066         }
0067 
0068         return result;
0069     }
0070 
0071     bool removePath(const QString &file) {
0072         bool result = true;
0073         const QString ufile = unifyFilePath(file);
0074 
0075         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_fileEntries.contains(ufile), false);
0076 
0077         if (m_fileEntries[ufile].numConnections == 1) {
0078             m_fileEntries.remove(ufile);
0079             result = m_watcher.removePath(ufile);
0080         } else {
0081             m_fileEntries[ufile].numConnections--;
0082         }
0083         return result;
0084     }
0085 
0086     QStringList files() const {
0087         return m_watcher.files();
0088     }
0089 
0090 private Q_SLOTS:
0091     void slotFileChanged(const QString &path) {
0092 
0093         KIS_SAFE_ASSERT_RECOVER_RETURN(m_fileEntries.contains(path));
0094 
0095         FileEntry &entry = m_fileEntries[path];
0096 
0097         // re-add the file after QSaveFile optimization
0098         if (!m_watcher.files().contains(path)) {
0099 
0100             if (QFileInfo(path).exists()) {
0101                 const FileState oldState = entry.state;
0102 
0103                 m_watcher.addPath(path);
0104                 entry.state = Exists;
0105 
0106                 if (oldState == Lost) {
0107                     emit fileExistsStateChanged(path, true);
0108                 } else {
0109                     emit fileChanged(path);
0110                 }
0111 
0112             } else {
0113 
0114                 if (entry.state == Exists) {
0115                     entry.state = Reattaching;
0116                     entry.lostTimer.start();
0117                     m_reattachmentCompressor.start();
0118 
0119                 } else if (entry.state == Reattaching) {
0120                     if (entry.lostTimer.elapsed() > 10000) {
0121                         entry.state = Lost;
0122                         m_lostCompressor.start();
0123                         emit fileExistsStateChanged(path, false);
0124                     } else {
0125                         m_reattachmentCompressor.start();
0126                     }
0127 
0128                 } else if (entry.state == Lost) {
0129                     m_lostCompressor.start();
0130                 }
0131 
0132 
0133 #if 0
0134                 const bool shouldSpitWarning =
0135                     absenceTimeMSec <= 600000 &&
0136                         ((absenceTimeMSec >= 60000 && (absenceTimeMSec % 60000 == 0)) ||
0137                          (absenceTimeMSec >= 10000 && (absenceTimeMSec % 10000 == 0)));
0138 
0139                 if (shouldSpitWarning) {
0140                     QString message;
0141                     QTextStream log(&message);
0142                     log.setCodec("UTF-8");
0143 
0144                     log << "WARNING: couldn't reconnect to a removed file layer's file (" << path << "). File is not available for " << absenceTimeMSec / 1000 << " seconds";
0145 
0146                     qWarning() << message;
0147                     KisUsageLogger::log(message);
0148 
0149                     if (absenceTimeMSec == 600000) {
0150                         message.clear();
0151                         log.reset();
0152 
0153                         log << "Giving up... :( No more reports about " << path;
0154 
0155                         qWarning() << message;
0156                         KisUsageLogger::log(message);
0157                     }
0158                 }
0159 #endif
0160             }
0161         } else {
0162             emit fileChanged(path);
0163         }
0164     }
0165 
0166     void slotFindLostFiles() {
0167         for (auto it = m_fileEntries.constBegin(); it != m_fileEntries.constEnd(); ++it) {
0168             if (it.value().state == Lost)
0169             slotFileChanged(it.key());
0170         }
0171     }
0172 
0173     void slotReattachFiles() {
0174         for (auto it = m_fileEntries.constBegin(); it != m_fileEntries.constEnd(); ++it) {
0175             if (it.value().state == Reattaching)
0176             slotFileChanged(it.key());
0177         }
0178     }
0179 
0180 
0181 Q_SIGNALS:
0182     void fileChanged(const QString &path);
0183     void fileExistsStateChanged(const QString &path, bool exists);
0184 
0185 public:
0186     static QString unifyFilePath(const QString &path) {
0187         return QFileInfo(path).absoluteFilePath();
0188     }
0189 
0190 private:
0191     QFileSystemWatcher m_watcher;
0192     QHash<QString, int> m_pathCount;
0193     KisSignalCompressor m_reattachmentCompressor;
0194     KisSignalCompressor m_lostCompressor;
0195     QHash<QString, int> m_lostFilesAbsenceCounter;
0196     QHash<QString, FileEntry> m_fileEntries;
0197 };
0198 
0199 Q_GLOBAL_STATIC(FileSystemWatcherWrapper, s_fileSystemWatcher)
0200 
0201 
0202 struct KisSafeDocumentLoader::Private
0203 {
0204     Private()
0205         : fileChangedSignalCompressor(500 /* ms */, KisSignalCompressor::POSTPONE)
0206     {
0207     }
0208 
0209     QScopedPointer<KisDocument> doc;
0210     KisSignalCompressor fileChangedSignalCompressor;
0211     bool isLoading = false;
0212     bool fileChangedFlag = false;
0213     QString path;
0214     QString temporaryPath;
0215 
0216     qint64 initialFileSize {0};
0217     QDateTime initialFileTimeStamp;
0218 
0219     int failureCount {0};
0220 };
0221 
0222 KisSafeDocumentLoader::KisSafeDocumentLoader(const QString &path, QObject *parent)
0223     : QObject(parent),
0224       m_d(new Private())
0225 {
0226     connect(s_fileSystemWatcher, SIGNAL(fileChanged(QString)),
0227             SLOT(fileChanged(QString)));
0228 
0229     connect(s_fileSystemWatcher, SIGNAL(fileExistsStateChanged(QString, bool)),
0230             SLOT(slotFileExistsStateChanged(QString, bool)));
0231 
0232     connect(&m_d->fileChangedSignalCompressor, SIGNAL(timeout()),
0233             SLOT(fileChangedCompressed()));
0234 
0235     setPath(path);
0236 }
0237 
0238 KisSafeDocumentLoader::~KisSafeDocumentLoader()
0239 {
0240     if (!m_d->path.isEmpty()) {
0241         s_fileSystemWatcher->removePath(m_d->path);
0242     }
0243 
0244     delete m_d;
0245 }
0246 
0247 void KisSafeDocumentLoader::setPath(const QString &path)
0248 {
0249     if (path.isEmpty()) return;
0250 
0251     if (!m_d->path.isEmpty()) {
0252         s_fileSystemWatcher->removePath(m_d->path);
0253     }
0254 
0255     m_d->path = path;
0256     s_fileSystemWatcher->addPath(m_d->path);
0257 }
0258 
0259 void KisSafeDocumentLoader::reloadImage()
0260 {
0261     fileChangedCompressed(true);
0262 }
0263 
0264 void KisSafeDocumentLoader::fileChanged(QString path)
0265 {
0266     if (FileSystemWatcherWrapper::unifyFilePath(m_d->path) == path) {
0267         m_d->fileChangedFlag = true;
0268         m_d->fileChangedSignalCompressor.start();
0269     }
0270 }
0271 
0272 void KisSafeDocumentLoader::slotFileExistsStateChanged(const QString &path, bool fileExists)
0273 {
0274     if (FileSystemWatcherWrapper::unifyFilePath(m_d->path) == path) {
0275         emit fileExistsStateChanged(fileExists);
0276         if (fileExists) {
0277             fileChanged(path);
0278         }
0279     }
0280 }
0281 
0282 void KisSafeDocumentLoader::fileChangedCompressed(bool sync)
0283 {
0284     if (m_d->isLoading) return;
0285 
0286     QFileInfo initialFileInfo(m_d->path);
0287     m_d->initialFileSize = initialFileInfo.size();
0288     m_d->initialFileTimeStamp = initialFileInfo.lastModified();
0289 
0290     // it may happen when the file is flushed by
0291     // so other application
0292     if (!m_d->initialFileSize) return;
0293 
0294     m_d->isLoading = true;
0295     m_d->fileChangedFlag = false;
0296 
0297     m_d->temporaryPath =
0298             QDir::tempPath() + '/' +
0299             QString("krita_file_layer_copy_%1_%2.%3")
0300             .arg(QApplication::applicationPid())
0301             .arg(qrand())
0302             .arg(initialFileInfo.suffix());
0303 
0304     QFile::copy(m_d->path, m_d->temporaryPath);
0305 
0306 
0307     if (!sync) {
0308         QTimer::singleShot(100, this, SLOT(delayedLoadStart()));
0309     } else {
0310         QApplication::processEvents();
0311         delayedLoadStart();
0312     }
0313 }
0314 
0315 void KisSafeDocumentLoader::delayedLoadStart()
0316 {
0317     QFileInfo originalInfo(m_d->path);
0318     QFileInfo tempInfo(m_d->temporaryPath);
0319     bool successfullyLoaded = false;
0320 
0321     if (!m_d->fileChangedFlag &&
0322             originalInfo.size() == m_d->initialFileSize &&
0323             originalInfo.lastModified() == m_d->initialFileTimeStamp &&
0324             tempInfo.size() == m_d->initialFileSize) {
0325 
0326         m_d->doc.reset(KisPart::instance()->createTemporaryDocument());
0327         m_d->doc->setFileBatchMode(true);
0328 
0329         if (m_d->path.toLower().endsWith("ora") || m_d->path.toLower().endsWith("kra")) {
0330             QScopedPointer<KoStore> store(KoStore::createStore(m_d->temporaryPath, KoStore::Read));
0331             if (store && !store->bad()) {
0332                 if (store->open(QString("mergedimage.png"))) {
0333                     QByteArray bytes = store->read(store->size());
0334                     store->close();
0335                     QImage mergedImage;
0336                     mergedImage.loadFromData(bytes);
0337                     Q_ASSERT(!mergedImage.isNull());
0338                     KisImageSP image = new KisImage(0, mergedImage.width(), mergedImage.height(), KoColorSpaceRegistry::instance()->rgb8(), "");
0339 
0340                     constexpr double DOTS_PER_METER_TO_DOTS_PER_INCH = 0.00035285815102328864;
0341                     double xres = mergedImage.dotsPerMeterX()  * DOTS_PER_METER_TO_DOTS_PER_INCH;
0342                     double yres = mergedImage.dotsPerMeterY() * DOTS_PER_METER_TO_DOTS_PER_INCH;
0343 
0344                     image->setResolution(xres, yres);
0345                     KisPaintLayerSP layer = new KisPaintLayer(image, "", OPACITY_OPAQUE_U8);
0346                     layer->paintDevice()->convertFromQImage(mergedImage, 0);
0347                     image->addNode(layer, image->rootLayer());
0348                     image->initialRefreshGraph();
0349                     m_d->doc->setCurrentImage(image);
0350                     successfullyLoaded = true;
0351                 }
0352                 else {
0353                     qWarning() << "delayedLoadStart: Could not open mergedimage.png";
0354                 }
0355             }
0356             else {
0357                 qWarning() << "delayedLoadStart: Store was bad";
0358             }
0359         }
0360         else {
0361             successfullyLoaded = m_d->doc->openPath(m_d->temporaryPath,
0362                                                    KisDocument::DontAddToRecent);
0363 
0364             if (successfullyLoaded) {
0365                 // Wait for required updates, if any. BUG: 448256
0366                 KisLayerUtils::forceAllDelayedNodesUpdate(m_d->doc->image()->root());
0367                 m_d->doc->image()->waitForDone();
0368             }
0369         }
0370     } else {
0371         dbgKrita << "File was modified externally. Restarting.";
0372         dbgKrita << ppVar(m_d->fileChangedFlag);
0373         dbgKrita << ppVar(m_d->initialFileSize);
0374         dbgKrita << ppVar(m_d->initialFileTimeStamp);
0375         dbgKrita << ppVar(originalInfo.size());
0376         dbgKrita << ppVar(originalInfo.lastModified());
0377         dbgKrita << ppVar(tempInfo.size());
0378     }
0379 
0380     QFile::remove(m_d->temporaryPath);
0381     m_d->isLoading = false;
0382 
0383     if (!successfullyLoaded) {
0384         // Restart the attempt
0385         m_d->failureCount++;
0386         if (m_d->failureCount >= 3) {
0387             emit loadingFailed();
0388         }
0389         else {
0390             m_d->fileChangedSignalCompressor.start();
0391         }
0392     }
0393     else {
0394         KisPaintDeviceSP paintDevice = new KisPaintDevice(m_d->doc->image()->colorSpace());
0395         KisPaintDeviceSP projection = m_d->doc->image()->projection();
0396         paintDevice->makeCloneFrom(projection, projection->extent());
0397         emit loadingFinished(paintDevice,
0398                              m_d->doc->image()->xRes(),
0399                              m_d->doc->image()->yRes(),
0400                              m_d->doc->image()->size());
0401     }
0402 
0403     m_d->doc.reset();
0404 }
0405 
0406 #include "kis_safe_document_loader.moc"