KTextEditor

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

KDE's Doxygen guidelines are available online.