KTextEditor

kateswapfile.cpp
1 /* SPDX-License-Identifier: LGPL-2.0-or-later
2 
3  Copyright (C) 2010-2018 Dominik Haumann <[email protected]>
4  Copyright (C) 2010 Diana-Victoria Tiriplica <[email protected]>
5 
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.
10 
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 GNU
14  Library General Public License for more details.
15 
16  You should have received a copy of the GNU Library General Public License
17  along with this library; see the file COPYING.LIB. If not, write to
18  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19  Boston, MA 02110-1301, USA.
20 */
21 
22 #include "config.h"
23 
24 #include "kateconfig.h"
25 #include "katepartdebug.h"
26 #include "kateswapdiffcreator.h"
27 #include "kateswapfile.h"
28 #include "kateundomanager.h"
29 
30 #include <ktexteditor/view.h>
31 
32 #include <KLocalizedString>
33 #include <KStandardGuiItem>
34 
35 #include <QApplication>
36 #include <QCryptographicHash>
37 #include <QDir>
38 #include <QFileInfo>
39 
40 #ifndef Q_OS_WIN
41 #include <unistd.h>
42 #endif
43 
44 // swap file version header
45 const static char swapFileVersionString[] = "Kate Swap File 2.0";
46 
47 // tokens for swap files
48 const static qint8 EA_StartEditing = 'S';
49 const static qint8 EA_FinishEditing = 'E';
50 const static qint8 EA_WrapLine = 'W';
51 const static qint8 EA_UnwrapLine = 'U';
52 const static qint8 EA_InsertText = 'I';
53 const static qint8 EA_RemoveText = 'R';
54 
55 namespace Kate
56 {
57 QTimer *SwapFile::s_timer = nullptr;
58 
59 SwapFile::SwapFile(KTextEditor::DocumentPrivate *document)
60  : QObject(document)
61  , m_document(document)
62  , m_trackingEnabled(false)
63  , m_recovered(false)
64  , m_needSync(false)
65 {
66  // fixed version of serialisation
67  m_stream.setVersion(QDataStream::Qt_4_6);
68 
69  // connect the timer
70  connect(syncTimer(), SIGNAL(timeout()), this, SLOT(writeFileToDisk()), Qt::DirectConnection);
71 
72  // connecting the signals
73  connect(&m_document->buffer(), SIGNAL(saved(QString)), this, SLOT(fileSaved(QString)));
74  connect(&m_document->buffer(), SIGNAL(loaded(QString, bool)), this, SLOT(fileLoaded(QString)));
75  connect(m_document, SIGNAL(configChanged()), this, SLOT(configChanged()));
76 
77  // tracking on!
78  setTrackingEnabled(true);
79 }
80 
81 SwapFile::~SwapFile()
82 {
83  // only remove swap file after data recovery (bug #304576)
84  if (!shouldRecover()) {
85  removeSwapFile();
86  }
87 }
88 
89 void SwapFile::configChanged()
90 {
91 }
92 
93 void SwapFile::setTrackingEnabled(bool enable)
94 {
95  if (m_trackingEnabled == enable) {
96  return;
97  }
98 
99  m_trackingEnabled = enable;
100 
101  TextBuffer &buffer = m_document->buffer();
102 
103  if (m_trackingEnabled) {
104  connect(&buffer, SIGNAL(editingStarted()), this, SLOT(startEditing()));
105  connect(&buffer, SIGNAL(editingFinished()), this, SLOT(finishEditing()));
106  connect(m_document, SIGNAL(modifiedChanged(KTextEditor::Document *)), this, SLOT(modifiedChanged()));
107 
108  connect(&buffer, SIGNAL(lineWrapped(KTextEditor::Cursor)), this, SLOT(wrapLine(KTextEditor::Cursor)));
109  connect(&buffer, SIGNAL(lineUnwrapped(int)), this, SLOT(unwrapLine(int)));
110  connect(&buffer, SIGNAL(textInserted(KTextEditor::Cursor, QString)), this, SLOT(insertText(KTextEditor::Cursor, QString)));
111  connect(&buffer, SIGNAL(textRemoved(KTextEditor::Range, QString)), this, SLOT(removeText(KTextEditor::Range)));
112  } else {
113  disconnect(&buffer, SIGNAL(editingStarted()), this, SLOT(startEditing()));
114  disconnect(&buffer, SIGNAL(editingFinished()), this, SLOT(finishEditing()));
115  disconnect(m_document, SIGNAL(modifiedChanged(KTextEditor::Document *)), this, SLOT(modifiedChanged()));
116 
117  disconnect(&buffer, SIGNAL(lineWrapped(KTextEditor::Cursor)), this, SLOT(wrapLine(KTextEditor::Cursor)));
118  disconnect(&buffer, SIGNAL(lineUnwrapped(int)), this, SLOT(unwrapLine(int)));
119  disconnect(&buffer, SIGNAL(textInserted(KTextEditor::Cursor, QString)), this, SLOT(insertText(KTextEditor::Cursor, QString)));
120  disconnect(&buffer, SIGNAL(textRemoved(KTextEditor::Range, QString)), this, SLOT(removeText(KTextEditor::Range)));
121  }
122 }
123 
124 void SwapFile::fileClosed()
125 {
126  // remove old swap file, file is now closed
127  if (!shouldRecover()) {
128  removeSwapFile();
129  } else {
130  m_document->setReadWrite(true);
131  }
132 
133  // purge filename
134  updateFileName();
135 }
136 
137 KTextEditor::DocumentPrivate *SwapFile::document()
138 {
139  return m_document;
140 }
141 
142 bool SwapFile::isValidSwapFile(QDataStream &stream, bool checkDigest) const
143 {
144  // read and check header
146  stream >> header;
147 
148  if (header != swapFileVersionString) {
149  qCWarning(LOG_KTE) << "Can't open swap file, wrong version";
150  return false;
151  }
152 
153  // read checksum
154  QByteArray checksum;
155  stream >> checksum;
156  // qCDebug(LOG_KTE) << "DIGEST:" << checksum << m_document->checksum();
157  if (checkDigest && checksum != m_document->checksum()) {
158  qCWarning(LOG_KTE) << "Can't recover from swap file, checksum of document has changed";
159  return false;
160  }
161 
162  return true;
163 }
164 
165 void SwapFile::fileLoaded(const QString &)
166 {
167  // look for swap file
168  if (!updateFileName()) {
169  return;
170  }
171 
172  if (!m_swapfile.exists()) {
173  // qCDebug(LOG_KTE) << "No swap file";
174  return;
175  }
176 
177  if (!QFileInfo(m_swapfile).isReadable()) {
178  qCWarning(LOG_KTE) << "Can't open swap file (missing permissions)";
179  return;
180  }
181 
182  // sanity check
183  QFile peekFile(fileName());
184  if (peekFile.open(QIODevice::ReadOnly)) {
185  QDataStream stream(&peekFile);
186  if (!isValidSwapFile(stream, true)) {
187  removeSwapFile();
188  return;
189  }
190  peekFile.close();
191  } else {
192  qCWarning(LOG_KTE) << "Can't open swap file:" << fileName();
193  return;
194  }
195 
196  // show swap file message
197  m_document->setReadWrite(false);
198  showSwapFileMessage();
199 }
200 
201 void SwapFile::modifiedChanged()
202 {
203  if (!m_document->isModified() && !shouldRecover()) {
204  m_needSync = false;
205  // the file is not modified and we are not in recover mode
206  removeSwapFile();
207  }
208 }
209 
210 void SwapFile::recover()
211 {
212  m_document->setReadWrite(true);
213 
214  // if isOpen() returns true, the swap file likely changed already (appended data)
215  // Example: The document was falsely marked as writable and the user changed
216  // text even though the recover bar was visible. In this case, a replay of
217  // the swap file across wrong document content would happen -> certainly wrong
218  if (m_swapfile.isOpen()) {
219  qCWarning(LOG_KTE) << "Attempt to recover an already modified document. Aborting";
220  removeSwapFile();
221  return;
222  }
223 
224  // if the file doesn't exist, abort (user might have deleted it, or use two editor instances)
225  if (!m_swapfile.open(QIODevice::ReadOnly)) {
226  qCWarning(LOG_KTE) << "Can't open swap file";
227  return;
228  }
229 
230  // remember that the file has recovered
231  m_recovered = true;
232 
233  // open data stream
234  m_stream.setDevice(&m_swapfile);
235 
236  // replay the swap file
237  bool success = recover(m_stream);
238 
239  // close swap file
240  m_stream.setDevice(nullptr);
241  m_swapfile.close();
242 
243  if (!success) {
244  removeSwapFile();
245  }
246 
247  // recover can also be called through the KTE::RecoveryInterface.
248  // Make sure, the message is hidden in this case as well.
249  if (m_swapMessage) {
250  m_swapMessage->deleteLater();
251  }
252 }
253 
254 bool SwapFile::recover(QDataStream &stream, bool checkDigest)
255 {
256  if (!isValidSwapFile(stream, checkDigest)) {
257  return false;
258  }
259 
260  // disconnect current signals
261  setTrackingEnabled(false);
262 
263  // needed to set undo/redo cursors in a sane way
264  bool firstEditInGroup = false;
267 
268  // replay swapfile
269  bool editRunning = false;
270  bool brokenSwapFile = false;
271  while (!stream.atEnd()) {
272  if (brokenSwapFile) {
273  break;
274  }
275 
276  qint8 type;
277  stream >> type;
278  switch (type) {
279  case EA_StartEditing: {
280  m_document->editStart();
281  editRunning = true;
282  firstEditInGroup = true;
283  undoCursor = KTextEditor::Cursor::invalid();
284  redoCursor = KTextEditor::Cursor::invalid();
285  break;
286  }
287  case EA_FinishEditing: {
288  m_document->editEnd();
289 
290  // empty editStart() / editEnd() groups exist: only set cursor if required
291  if (!firstEditInGroup) {
292  // set undo/redo cursor of last KateUndoGroup of the undo manager
293  m_document->undoManager()->setUndoRedoCursorsOfLastGroup(undoCursor, redoCursor);
294  m_document->undoManager()->undoSafePoint();
295  }
296  firstEditInGroup = false;
297  editRunning = false;
298  break;
299  }
300  case EA_WrapLine: {
301  if (!editRunning) {
302  brokenSwapFile = true;
303  break;
304  }
305 
306  int line = 0, column = 0;
307  stream >> line >> column;
308 
309  // emulate buffer unwrapLine with document
310  m_document->editWrapLine(line, column, true);
311 
312  // track undo/redo cursor
313  if (firstEditInGroup) {
314  firstEditInGroup = false;
315  undoCursor = KTextEditor::Cursor(line, column);
316  }
317  redoCursor = KTextEditor::Cursor(line + 1, 0);
318 
319  break;
320  }
321  case EA_UnwrapLine: {
322  if (!editRunning) {
323  brokenSwapFile = true;
324  break;
325  }
326 
327  int line = 0;
328  stream >> line;
329 
330  // assert valid line
331  Q_ASSERT(line > 0);
332 
333  const int undoColumn = m_document->lineLength(line - 1);
334 
335  // emulate buffer unwrapLine with document
336  m_document->editUnWrapLine(line - 1, true, 0);
337 
338  // track undo/redo cursor
339  if (firstEditInGroup) {
340  firstEditInGroup = false;
341  undoCursor = KTextEditor::Cursor(line, 0);
342  }
343  redoCursor = KTextEditor::Cursor(line - 1, undoColumn);
344 
345  break;
346  }
347  case EA_InsertText: {
348  if (!editRunning) {
349  brokenSwapFile = true;
350  break;
351  }
352 
353  int line, column;
354  QByteArray text;
355  stream >> line >> column >> text;
356  m_document->insertText(KTextEditor::Cursor(line, column), QString::fromUtf8(text.data(), text.size()));
357 
358  // track undo/redo cursor
359  if (firstEditInGroup) {
360  firstEditInGroup = false;
361  undoCursor = KTextEditor::Cursor(line, column);
362  }
363  redoCursor = KTextEditor::Cursor(line, column + text.size());
364 
365  break;
366  }
367  case EA_RemoveText: {
368  if (!editRunning) {
369  brokenSwapFile = true;
370  break;
371  }
372 
373  int line, startColumn, endColumn;
374  stream >> line >> startColumn >> endColumn;
375  m_document->removeText(KTextEditor::Range(KTextEditor::Cursor(line, startColumn), KTextEditor::Cursor(line, endColumn)));
376 
377  // track undo/redo cursor
378  if (firstEditInGroup) {
379  firstEditInGroup = false;
380  undoCursor = KTextEditor::Cursor(line, endColumn);
381  }
382  redoCursor = KTextEditor::Cursor(line, startColumn);
383 
384  break;
385  }
386  default: {
387  qCWarning(LOG_KTE) << "Unknown type:" << type;
388  }
389  }
390  }
391 
392  // balanced editStart and editEnd?
393  if (editRunning) {
394  brokenSwapFile = true;
395  m_document->editEnd();
396  }
397 
398  // warn the user if the swap file is not complete
399  if (brokenSwapFile) {
400  qCWarning(LOG_KTE) << "Some data might be lost";
401  } else {
402  // set sane final cursor, if possible
403  KTextEditor::View *view = m_document->activeView();
404  redoCursor = m_document->undoManager()->lastRedoCursor();
405  if (view && redoCursor.isValid()) {
406  view->setCursorPosition(redoCursor);
407  }
408  }
409 
410  // reconnect the signals
411  setTrackingEnabled(true);
412 
413  return true;
414 }
415 
416 void SwapFile::fileSaved(const QString &)
417 {
418  m_needSync = false;
419 
420  // remove old swap file (e.g. if a file A was "saved as" B)
421  removeSwapFile();
422 
423  // set the name for the new swap file
424  updateFileName();
425 }
426 
427 void SwapFile::startEditing()
428 {
429  // no swap file, no work
430  if (m_swapfile.fileName().isEmpty()) {
431  return;
432  }
433 
434  // if swap file doesn't exists, open it in WriteOnly mode
435  // if it does, append the data to the existing swap file,
436  // in case you recover and start editing again
437  if (!m_swapfile.exists()) {
438  // create path if not there
439  if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory && !QDir(KateDocumentConfig::global()->swapDirectory()).exists()) {
440  QDir().mkpath(KateDocumentConfig::global()->swapDirectory());
441  }
442 
443  m_swapfile.open(QIODevice::WriteOnly);
444  m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
445  m_stream.setDevice(&m_swapfile);
446 
447  // write file header
448  m_stream << QByteArray(swapFileVersionString);
449 
450  // write checksum
451  m_stream << m_document->checksum();
452  } else if (m_stream.device() == nullptr) {
453  m_swapfile.open(QIODevice::Append);
454  m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
455  m_stream.setDevice(&m_swapfile);
456  }
457 
458  // format: qint8
459  m_stream << EA_StartEditing;
460 }
461 
462 void SwapFile::finishEditing()
463 {
464  // skip if not open
465  if (!m_swapfile.isOpen()) {
466  return;
467  }
468 
469  // write the file to the disk every 15 seconds (default)
470  // skip this if we disabled that
471  if (m_document->config()->swapSyncInterval() != 0 && !syncTimer()->isActive()) {
472  // important: we store the interval as seconds, start wants milliseconds!
473  syncTimer()->start(m_document->config()->swapSyncInterval() * 1000);
474  }
475 
476  // format: qint8
477  m_stream << EA_FinishEditing;
478  m_swapfile.flush();
479 }
480 
481 void SwapFile::wrapLine(const KTextEditor::Cursor &position)
482 {
483  // skip if not open
484  if (!m_swapfile.isOpen()) {
485  return;
486  }
487 
488  // format: qint8, int, int
489  m_stream << EA_WrapLine << position.line() << position.column();
490 
491  m_needSync = true;
492 }
493 
494 void SwapFile::unwrapLine(int line)
495 {
496  // skip if not open
497  if (!m_swapfile.isOpen()) {
498  return;
499  }
500 
501  // format: qint8, int
502  m_stream << EA_UnwrapLine << line;
503 
504  m_needSync = true;
505 }
506 
507 void SwapFile::insertText(const KTextEditor::Cursor &position, const QString &text)
508 {
509  // skip if not open
510  if (!m_swapfile.isOpen()) {
511  return;
512  }
513 
514  // format: qint8, int, int, bytearray
515  m_stream << EA_InsertText << position.line() << position.column() << text.toUtf8();
516 
517  m_needSync = true;
518 }
519 
520 void SwapFile::removeText(const KTextEditor::Range &range)
521 {
522  // skip if not open
523  if (!m_swapfile.isOpen()) {
524  return;
525  }
526 
527  // format: qint8, int, int, int
528  Q_ASSERT(range.start().line() == range.end().line());
529  m_stream << EA_RemoveText << range.start().line() << range.start().column() << range.end().column();
530 
531  m_needSync = true;
532 }
533 
534 bool SwapFile::shouldRecover() const
535 {
536  // should not recover if the file has already recovered in another view
537  if (m_recovered) {
538  return false;
539  }
540 
541  return !m_swapfile.fileName().isEmpty() && m_swapfile.exists() && m_stream.device() == nullptr;
542 }
543 
544 void SwapFile::discard()
545 {
546  m_document->setReadWrite(true);
547  removeSwapFile();
548 
549  // discard can also be called through the KTE::RecoveryInterface.
550  // Make sure, the message is hidden in this case as well.
551  if (m_swapMessage) {
552  m_swapMessage->deleteLater();
553  }
554 }
555 
556 void SwapFile::removeSwapFile()
557 {
558  if (!m_swapfile.fileName().isEmpty() && m_swapfile.exists()) {
559  m_stream.setDevice(nullptr);
560  m_swapfile.close();
561  m_swapfile.remove();
562  }
563 }
564 
565 bool SwapFile::updateFileName()
566 {
567  // first clear filename
568  m_swapfile.setFileName(QString());
569 
570  // get the new path
571  QString path = fileName();
572  if (path.isNull()) {
573  return false;
574  }
575 
576  m_swapfile.setFileName(path);
577  return true;
578 }
579 
580 QString SwapFile::fileName()
581 {
582  const QUrl &url = m_document->url();
583  if (url.isEmpty() || !url.isLocalFile()) {
584  return QString();
585  }
586 
587  const QString fullLocalPath(url.toLocalFile());
588  QString path;
589  if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory) {
590  path = KateDocumentConfig::global()->swapDirectory();
591  path.append(QLatin1Char('/'));
592 
593  // append the sha1 sum of the full path + filename, to avoid "too long" paths created
594  path.append(QString::fromLatin1(QCryptographicHash::hash(fullLocalPath.toUtf8(), QCryptographicHash::Sha1).toHex()));
595  path.append(QLatin1Char('-'));
596  path.append(QFileInfo(fullLocalPath).fileName());
597 
598  path.append(QLatin1String(".kate-swp"));
599  } else {
600  path = fullLocalPath;
601  int poz = path.lastIndexOf(QLatin1Char('/'));
602  path.insert(poz + 1, QLatin1Char('.'));
603  path.append(QLatin1String(".kate-swp"));
604  }
605 
606  return path;
607 }
608 
609 QTimer *SwapFile::syncTimer()
610 {
611  if (s_timer == nullptr) {
612  s_timer = new QTimer(QApplication::instance());
613  s_timer->setSingleShot(true);
614  }
615 
616  return s_timer;
617 }
618 
619 void SwapFile::writeFileToDisk()
620 {
621  if (m_needSync) {
622  m_needSync = false;
623 
624 #ifndef Q_OS_WIN
625  // ensure that the file is written to disk
626 #if HAVE_FDATASYNC
627  fdatasync(m_swapfile.handle());
628 #else
629  fsync(m_swapfile.handle());
630 #endif
631 #endif
632  }
633 }
634 
635 void SwapFile::showSwapFileMessage()
636 {
637  m_swapMessage = new KTextEditor::Message(i18n("The file was not closed properly."), KTextEditor::Message::Warning);
638  m_swapMessage->setWordWrap(true);
639 
640  QAction *diffAction = new QAction(QIcon::fromTheme(QStringLiteral("split")), i18n("View Changes"), nullptr);
641  QAction *recoverAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo")), i18n("Recover Data"), nullptr);
642  QAction *discardAction = new QAction(KStandardGuiItem::discard().icon(), i18n("Discard"), nullptr);
643 
644  m_swapMessage->addAction(diffAction, false);
645  m_swapMessage->addAction(recoverAction);
646  m_swapMessage->addAction(discardAction);
647 
648  connect(diffAction, SIGNAL(triggered()), SLOT(showDiff()));
649  connect(recoverAction, SIGNAL(triggered()), SLOT(recover()), Qt::QueuedConnection);
650  connect(discardAction, SIGNAL(triggered()), SLOT(discard()), Qt::QueuedConnection);
651 
652  m_document->postMessage(m_swapMessage);
653 }
654 
655 void SwapFile::showDiff()
656 {
657  // the diff creator deletes itself through deleteLater() when it's done
658  SwapDiffCreator *diffCreator = new SwapDiffCreator(this);
659  diffCreator->viewDiff();
660 }
661 
662 }
QString url(QUrl::FormattingOptions options) const const
bool isReadable() const const
QString & append(QChar ch)
virtual bool setCursorPosition(Cursor position)=0
Set the view&#39;s new cursor to position.
The Cursor represents a position in a Document.
Definition: cursor.h:85
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
bool isEmpty() const const
static constexpr Cursor invalid() Q_DECL_NOEXCEPT
Returns an invalid cursor.
Definition: cursor.h:123
constexpr bool isValid() const Q_DECL_NOEXCEPT
Returns whether the current position of this cursor is a valid position (line + column must both be >...
Definition: cursor.h:113
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
bool isNull() const const
bool exists() const const
QString fromUtf8(const char *str, int size)
A KParts derived class representing a text document.
Definition: document.h:199
Type type(const QSqlDatabase &db)
constexpr int column() const Q_DECL_NOEXCEPT
Retrieve the column on which this cursor is situated.
Definition: cursor.h:217
QCoreApplication * instance()
constexpr Cursor start() const Q_DECL_NOEXCEPT
Get the start position of this range.
bool atEnd() const const
QString toLocalFile() const const
An object representing a section of text, from one Cursor to another.
KGuiItem discard()
QString i18n(const char *text, const TYPE &arg...)
constexpr Cursor end() const Q_DECL_NOEXCEPT
Get the end position of this range.
constexpr int line() const Q_DECL_NOEXCEPT
Retrieve the line on which this cursor is situated.
Definition: cursor.h:199
QByteArray hash(const QByteArray &data, QCryptographicHash::Algorithm method)
QString fromLatin1(const char *str, int size)
QIcon fromTheme(const QString &name)
QHeaderView * header() const const
A text widget with KXMLGUIClient that represents a Document.
Definition: view.h:155
DirectConnection
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Mark class containing line and mark types.
Definition: message.h:107
warning message type
Definition: message.h:123
bool mkpath(const QString &dirPath) const const
bool isLocalFile() const const
QByteArray toUtf8() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Mon Jul 6 2020 22:56:58 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.