| 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 |
| 36 | const static char swapFileVersionString[] = "Kate Swap File 2.0" ; |
| 37 | |
| 38 | // tokens for swap files |
| 39 | const static qint8 EA_StartEditing = 'S'; |
| 40 | const static qint8 EA_FinishEditing = 'E'; |
| 41 | const static qint8 EA_WrapLine = 'W'; |
| 42 | const static qint8 EA_UnwrapLine = 'U'; |
| 43 | const static qint8 EA_InsertText = 'I'; |
| 44 | const static qint8 EA_RemoveText = 'R'; |
| 45 | |
| 46 | namespace Kate |
| 47 | { |
| 48 | QTimer *SwapFile::s_timer = nullptr; |
| 49 | |
| 50 | SwapFile::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(sender: syncTimer(), signal: &QTimer::timeout, context: this, slot: &Kate::SwapFile::writeFileToDisk, type: Qt::DirectConnection); |
| 62 | |
| 63 | // connecting the signals |
| 64 | connect(sender: &m_document->buffer(), signal: &KateBuffer::saved, context: this, slot: &Kate::SwapFile::fileSaved); |
| 65 | connect(sender: &m_document->buffer(), signal: &KateBuffer::loaded, context: this, slot: &Kate::SwapFile::fileLoaded); |
| 66 | connect(sender: m_document, signal: &KTextEditor::Document::configChanged, context: this, slot: &SwapFile::configChanged); |
| 67 | |
| 68 | // tracking on! |
| 69 | setTrackingEnabled(true); |
| 70 | } |
| 71 | |
| 72 | SwapFile::~SwapFile() |
| 73 | { |
| 74 | // only remove swap file after data recovery (bug #304576) |
| 75 | if (!shouldRecover()) { |
| 76 | removeSwapFile(); |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | void SwapFile::configChanged() |
| 81 | { |
| 82 | } |
| 83 | |
| 84 | void 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(sender: m_document, signal: &KTextEditor::Document::editingStarted, context: this, slot: &Kate::SwapFile::startEditing); |
| 94 | connect(sender: m_document, signal: &KTextEditor::Document::editingFinished, context: this, slot: &Kate::SwapFile::finishEditing); |
| 95 | connect(sender: m_document, signal: &KTextEditor::DocumentPrivate::modifiedChanged, context: this, slot: &SwapFile::modifiedChanged); |
| 96 | |
| 97 | connect(sender: m_document, signal: &KTextEditor::Document::lineWrapped, context: this, slot: &Kate::SwapFile::wrapLine); |
| 98 | connect(sender: m_document, signal: &KTextEditor::Document::lineUnwrapped, context: this, slot: &Kate::SwapFile::unwrapLine); |
| 99 | connect(sender: m_document, signal: &KTextEditor::Document::textInserted, context: this, slot: &Kate::SwapFile::insertText); |
| 100 | connect(sender: m_document, signal: &KTextEditor::Document::textRemoved, context: this, slot: &Kate::SwapFile::removeText); |
| 101 | } else { |
| 102 | disconnect(sender: m_document, signal: &KTextEditor::Document::editingStarted, receiver: this, slot: &Kate::SwapFile::startEditing); |
| 103 | disconnect(sender: m_document, signal: &KTextEditor::Document::editingFinished, receiver: this, slot: &Kate::SwapFile::finishEditing); |
| 104 | disconnect(sender: m_document, signal: &KTextEditor::DocumentPrivate::modifiedChanged, receiver: this, slot: &SwapFile::modifiedChanged); |
| 105 | |
| 106 | disconnect(sender: m_document, signal: &KTextEditor::Document::lineWrapped, receiver: this, slot: &Kate::SwapFile::wrapLine); |
| 107 | disconnect(sender: m_document, signal: &KTextEditor::Document::lineUnwrapped, receiver: this, slot: &Kate::SwapFile::unwrapLine); |
| 108 | disconnect(sender: m_document, signal: &KTextEditor::Document::textInserted, receiver: this, slot: &Kate::SwapFile::insertText); |
| 109 | disconnect(sender: m_document, signal: &KTextEditor::Document::textRemoved, receiver: this, slot: &Kate::SwapFile::removeText); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | void 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 | |
| 126 | KTextEditor::DocumentPrivate *SwapFile::document() |
| 127 | { |
| 128 | return m_document; |
| 129 | } |
| 130 | |
| 131 | bool SwapFile::isValidSwapFile(QDataStream &stream, bool checkDigest) const |
| 132 | { |
| 133 | // read and check header |
| 134 | QByteArray ; |
| 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 | |
| 154 | void 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(flags: QIODevice::ReadOnly)) { |
| 174 | QDataStream stream(&peekFile); |
| 175 | if (!isValidSwapFile(stream, checkDigest: 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 | |
| 190 | void 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 | |
| 198 | void 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(flags: 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 | |
| 242 | bool 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; |
| 253 | KTextEditor::Cursor undoCursor = KTextEditor::Cursor::invalid(); |
| 254 | KTextEditor::Cursor redoCursor = KTextEditor::Cursor::invalid(); |
| 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, col: column, newLine: 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: line - 1); |
| 323 | |
| 324 | // emulate buffer unwrapLine with document |
| 325 | m_document->editUnWrapLine(line: line - 1, removeLine: true, length: 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 | QString textStr = QString::fromUtf8(utf8: text.data(), size: text.size()); |
| 347 | m_document->insertText(position: KTextEditor::Cursor(line, column), s: textStr); |
| 348 | |
| 349 | // track undo/redo cursor |
| 350 | if (firstEditInGroup) { |
| 351 | firstEditInGroup = false; |
| 352 | undoCursor = KTextEditor::Cursor(line, column); |
| 353 | } |
| 354 | redoCursor = KTextEditor::Cursor(line, column + textStr.length()); |
| 355 | |
| 356 | break; |
| 357 | } |
| 358 | case EA_RemoveText: { |
| 359 | if (!editRunning) { |
| 360 | brokenSwapFile = true; |
| 361 | break; |
| 362 | } |
| 363 | |
| 364 | int line; |
| 365 | int startColumn; |
| 366 | int endColumn; |
| 367 | stream >> line >> startColumn >> endColumn; |
| 368 | m_document->removeText(range: KTextEditor::Range(KTextEditor::Cursor(line, startColumn), KTextEditor::Cursor(line, endColumn))); |
| 369 | |
| 370 | // track undo/redo cursor |
| 371 | if (firstEditInGroup) { |
| 372 | firstEditInGroup = false; |
| 373 | undoCursor = KTextEditor::Cursor(line, endColumn); |
| 374 | } |
| 375 | redoCursor = KTextEditor::Cursor(line, startColumn); |
| 376 | |
| 377 | break; |
| 378 | } |
| 379 | default: { |
| 380 | qCWarning(LOG_KTE) << "Unknown type:" << type; |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | // balanced editStart and editEnd? |
| 386 | if (editRunning) { |
| 387 | brokenSwapFile = true; |
| 388 | m_document->editEnd(); |
| 389 | } |
| 390 | |
| 391 | // warn the user if the swap file is not complete |
| 392 | if (brokenSwapFile) { |
| 393 | qCWarning(LOG_KTE) << "Some data might be lost" ; |
| 394 | } else { |
| 395 | // set sane final cursor, if possible |
| 396 | KTextEditor::View *view = m_document->activeView(); |
| 397 | redoCursor = m_document->undoManager()->lastRedoCursor(); |
| 398 | if (view && redoCursor.isValid()) { |
| 399 | view->setCursorPosition(redoCursor); |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | // reconnect the signals |
| 404 | setTrackingEnabled(true); |
| 405 | |
| 406 | return true; |
| 407 | } |
| 408 | |
| 409 | void SwapFile::fileSaved(const QString &) |
| 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 | |
| 418 | void 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(dirPath: KateDocumentConfig::global()->swapDirectory()); |
| 433 | } |
| 434 | |
| 435 | m_swapfile.open(flags: 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(flags: 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 | m_needSync = true; |
| 453 | } |
| 454 | |
| 455 | void SwapFile::finishEditing() |
| 456 | { |
| 457 | // skip if not open |
| 458 | if (!m_swapfile.isOpen()) { |
| 459 | return; |
| 460 | } |
| 461 | |
| 462 | // write the file to the disk every 15 seconds (default) |
| 463 | // skip this if we disabled that |
| 464 | if (m_document->config()->swapSyncInterval() != 0 && !syncTimer()->isActive()) { |
| 465 | // important: we store the interval as seconds, start wants milliseconds! |
| 466 | syncTimer()->start(msec: m_document->config()->swapSyncInterval() * 1000); |
| 467 | } |
| 468 | |
| 469 | // format: qint8 |
| 470 | m_stream << EA_FinishEditing; |
| 471 | m_needSync = true; |
| 472 | } |
| 473 | |
| 474 | void SwapFile::wrapLine(KTextEditor::Document *, const KTextEditor::Cursor position) |
| 475 | { |
| 476 | // skip if not open |
| 477 | if (!m_swapfile.isOpen()) { |
| 478 | return; |
| 479 | } |
| 480 | |
| 481 | // format: qint8, int, int |
| 482 | m_stream << EA_WrapLine << position.line() << position.column(); |
| 483 | m_needSync = true; |
| 484 | } |
| 485 | |
| 486 | void 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 | m_needSync = true; |
| 496 | } |
| 497 | |
| 498 | void SwapFile::insertText(KTextEditor::Document *, const KTextEditor::Cursor position, const QString &text) |
| 499 | { |
| 500 | // skip if not open |
| 501 | if (!m_swapfile.isOpen()) { |
| 502 | return; |
| 503 | } |
| 504 | |
| 505 | // format: qint8, int, int, bytearray |
| 506 | m_stream << EA_InsertText << position.line() << position.column() << text.toUtf8(); |
| 507 | m_needSync = true; |
| 508 | } |
| 509 | |
| 510 | void SwapFile::removeText(KTextEditor::Document *, KTextEditor::Range range, const QString &) |
| 511 | { |
| 512 | // skip if not open |
| 513 | if (!m_swapfile.isOpen()) { |
| 514 | return; |
| 515 | } |
| 516 | |
| 517 | // format: qint8, int, int, int |
| 518 | Q_ASSERT(range.start().line() == range.end().line()); |
| 519 | m_stream << EA_RemoveText << range.start().line() << range.start().column() << range.end().column(); |
| 520 | m_needSync = true; |
| 521 | } |
| 522 | |
| 523 | bool SwapFile::shouldRecover() const |
| 524 | { |
| 525 | // should not recover if the file has already recovered in another view |
| 526 | if (m_recovered) { |
| 527 | return false; |
| 528 | } |
| 529 | |
| 530 | return !m_swapfile.fileName().isEmpty() && m_swapfile.exists() && m_stream.device() == nullptr; |
| 531 | } |
| 532 | |
| 533 | void SwapFile::discard() |
| 534 | { |
| 535 | m_document->setReadWrite(true); |
| 536 | removeSwapFile(); |
| 537 | |
| 538 | // discard can also be called through the KTE::RecoveryInterface. |
| 539 | // Make sure, the message is hidden in this case as well. |
| 540 | if (m_swapMessage) { |
| 541 | m_swapMessage->deleteLater(); |
| 542 | } |
| 543 | } |
| 544 | |
| 545 | void SwapFile::removeSwapFile() |
| 546 | { |
| 547 | // ensure we have no stray sync |
| 548 | m_needSync = false; |
| 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 | |
| 557 | bool 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 | |
| 572 | QString 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()); |
| 580 | QString path; |
| 581 | if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory) { |
| 582 | path = KateDocumentConfig::global()->swapDirectory(); |
| 583 | path.append(c: QLatin1Char('/')); |
| 584 | |
| 585 | // append the sha1 sum of the full path + filename, to avoid "too long" paths created |
| 586 | path.append(s: QString::fromLatin1(ba: QCryptographicHash::hash(data: fullLocalPath.toUtf8(), method: QCryptographicHash::Sha1).toHex())); |
| 587 | path.append(c: QLatin1Char('-')); |
| 588 | path.append(s: QFileInfo(fullLocalPath).fileName()); |
| 589 | |
| 590 | path.append(s: QLatin1String(".kate-swp" )); |
| 591 | } else { |
| 592 | path = fullLocalPath; |
| 593 | int poz = path.lastIndexOf(c: QLatin1Char('/')); |
| 594 | path.insert(i: poz + 1, c: QLatin1Char('.')); |
| 595 | path.append(s: QLatin1String(".kate-swp" )); |
| 596 | } |
| 597 | |
| 598 | return path; |
| 599 | } |
| 600 | |
| 601 | QTimer *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 | |
| 611 | void SwapFile::writeFileToDisk() |
| 612 | { |
| 613 | if (m_needSync) { |
| 614 | m_needSync = false; |
| 615 | |
| 616 | // ensure buffers are flushed first |
| 617 | m_swapfile.flush(); |
| 618 | |
| 619 | #ifndef Q_OS_WIN |
| 620 | // ensure that the file is written to disk |
| 621 | #if HAVE_FDATASYNC |
| 622 | fdatasync(fildes: m_swapfile.handle()); |
| 623 | #else |
| 624 | fsync(m_swapfile.handle()); |
| 625 | #endif |
| 626 | #else |
| 627 | _commit(m_swapfile.handle()); |
| 628 | #endif |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | void SwapFile::showSwapFileMessage() |
| 633 | { |
| 634 | m_swapMessage = new KTextEditor::Message(i18n("The file was not closed properly." ), KTextEditor::Message::Warning); |
| 635 | m_swapMessage->setWordWrap(true); |
| 636 | |
| 637 | QAction *diffAction = new QAction(QIcon::fromTheme(QStringLiteral("split" )), i18n("View Changes" ), nullptr); |
| 638 | QAction *recoverAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo" )), i18n("Recover Data" ), nullptr); |
| 639 | QAction *discardAction = new QAction(KStandardGuiItem::discard().icon(), i18n("Discard" ), nullptr); |
| 640 | |
| 641 | m_swapMessage->addAction(action: diffAction, closeOnTrigger: false); |
| 642 | m_swapMessage->addAction(action: recoverAction); |
| 643 | m_swapMessage->addAction(action: discardAction); |
| 644 | |
| 645 | connect(sender: diffAction, signal: &QAction::triggered, context: this, slot: &SwapFile::showDiff); |
| 646 | connect(sender: recoverAction, signal: &QAction::triggered, context: this, slot: qOverload<>(&Kate::SwapFile::recover), type: Qt::QueuedConnection); |
| 647 | connect(sender: discardAction, signal: &QAction::triggered, context: this, slot: &SwapFile::discard, type: Qt::QueuedConnection); |
| 648 | |
| 649 | m_document->postMessage(message: m_swapMessage); |
| 650 | } |
| 651 | |
| 652 | void SwapFile::showDiff() |
| 653 | { |
| 654 | // the diff creator deletes itself through deleteLater() when it's done |
| 655 | SwapDiffCreator *diffCreator = new SwapDiffCreator(this); |
| 656 | diffCreator->viewDiff(); |
| 657 | } |
| 658 | |
| 659 | } |
| 660 | |