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

KDE's Doxygen guidelines are available online.