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"