Browse Source

support backup file

Add configs:

- backup_directory
- backup_extension
- enable_backup_file
Le Tan 8 years ago
parent
commit
e6ce66ec7d
15 changed files with 411 additions and 17 deletions
  1. 11 1
      src/resources/vnote.ini
  2. 8 1
      src/utils/vutils.cpp
  3. 12 0
      src/vconfigmanager.cpp
  4. 33 0
      src/vconfigmanager.h
  5. 5 0
      src/veditor.h
  6. 7 1
      src/vedittab.cpp
  7. 12 0
      src/vedittab.h
  8. 15 0
      src/veditwindow.cpp
  9. 3 0
      src/veditwindow.h
  10. 98 0
      src/vfile.cpp
  11. 27 0
      src/vfile.h
  12. 24 7
      src/vmdeditor.cpp
  13. 4 0
      src/vmdeditor.h
  14. 134 7
      src/vmdtab.cpp
  15. 18 0
      src/vmdtab.h

+ 11 - 1
src/resources/vnote.ini

@@ -116,7 +116,7 @@ tool_bar_icon_size=18
 ; Markdown-it options
 ; Enable HTML tags in source
 markdownit_opt_html=true
-; Convert '\n' in paragraphs into <br>
+; Convert '\n' in paragraphs into <br/>
 markdownit_opt_breaks=false
 ; Auto-convert URL-like text to links
 markdownit_opt_linkify=true
@@ -155,6 +155,16 @@ startup_pages=
 ; Timer interval to check file modification or save file to tmp file in milliseconds
 file_timer_interval=2000
 
+; Directory for the backup file
+; A directory "." means to put the backup file in the same directory as the edited file
+backup_directory=.
+
+; String which is appended to a file name to make the name of the backup file
+backup_extension=.vswp
+
+; Enable back file
+enable_backup_file=true
+
 [web]
 ; Location and configuration for Mathjax
 mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML

+ 8 - 1
src/utils/vutils.cpp

@@ -891,7 +891,14 @@ bool VUtils::deleteFile(const VNotebook *p_notebook,
 bool VUtils::deleteFile(const QString &p_path)
 {
     QFile file(p_path);
-    return file.remove();
+    bool ret = file.remove();
+    if (ret) {
+        qDebug() << "deleted file" << p_path;
+    } else {
+        qWarning() << "fail to delete file" << p_path;
+    }
+
+    return ret;
 }
 
 bool VUtils::deleteFile(const VOrphanFile *p_file,

+ 12 - 0
src/vconfigmanager.cpp

@@ -268,6 +268,18 @@ void VConfigManager::initialize()
     if (m_fileTimerInterval < 100) {
         m_fileTimerInterval = 100;
     }
+
+    m_backupDirectory = getConfigFromSettings("global",
+                                              "backup_directory").toString();
+
+    m_backupExtension = getConfigFromSettings("global",
+                                              "backup_extension").toString();
+    if (m_backupExtension.isEmpty()) {
+        m_backupExtension = ".";
+    }
+
+    m_enableBackupFile = getConfigFromSettings("global",
+                                               "enable_backup_file").toBool();
 }
 
 void VConfigManager::initSettings()

+ 33 - 0
src/vconfigmanager.h

@@ -375,6 +375,15 @@ public:
     // Return the timer interval for checking file.
     int getFileTimerInterval() const;
 
+    // Get the backup directory.
+    const QString &getBackupDirectory() const;
+
+    // Get the backup file extension.
+    const QString &getBackupExtension() const;
+
+    // Whether backup file is enabled.
+    bool getEnableBackupFile() const;
+
 private:
     // Look up a config from user and default settings.
     QVariant getConfigFromSettings(const QString &section, const QString &key) const;
@@ -709,6 +718,15 @@ private:
     // Timer interval to check file in milliseconds.
     int m_fileTimerInterval;
 
+    // Directory for the backup file (relative or absolute path).
+    QString m_backupDirectory;
+
+    // Extension of the backup file.
+    QString m_backupExtension;
+
+    // Whether enable backup file.
+    bool m_enableBackupFile;
+
     // The name of the config file in each directory, obsolete.
     // Use c_dirConfigFile instead.
     static const QString c_obsoleteDirConfigFile;
@@ -1790,4 +1808,19 @@ inline int VConfigManager::getFileTimerInterval() const
     return m_fileTimerInterval;
 }
 
+inline const QString &VConfigManager::getBackupDirectory() const
+{
+    return m_backupDirectory;
+}
+
+inline const QString &VConfigManager::getBackupExtension() const
+{
+    return m_backupExtension;
+}
+
+inline bool VConfigManager::getEnableBackupFile() const
+{
+    return m_enableBackupFile;
+}
+
 #endif // VCONFIGMANAGER_H

+ 5 - 0
src/veditor.h

@@ -139,6 +139,11 @@ public:
 
     void setVimMode(VimMode p_mode);
 
+    virtual QString getContent() const = 0;
+
+    // @p_modified: if true, delete the whole content and insert the new content.
+    virtual void setContent(const QString &p_content, bool p_modified = false) = 0;
+
 // Wrapper functions for QPlainTextEdit/QTextEdit.
 // Ends with W to distinguish it from the original interfaces.
 public:

+ 7 - 1
src/vedittab.cpp

@@ -15,7 +15,9 @@ VEditTab::VEditTab(VFile *p_file, VEditArea *p_editArea, QWidget *p_parent)
       m_currentHeader(p_file, -1),
       m_editArea(p_editArea),
       m_checkFileChange(true),
-      m_fileDiverged(false)
+      m_fileDiverged(false),
+      m_ready(0),
+      m_enableBackupFile(g_config->getEnableBackupFile())
 {
     connect(qApp, &QApplication::focusChanged,
             this, &VEditTab::handleFocusChanged);
@@ -181,3 +183,7 @@ void VEditTab::reloadFromDisk()
     m_checkFileChange = true;
     reload();
 }
+
+void VEditTab::writeBackupFile()
+{
+}

+ 12 - 0
src/vedittab.h

@@ -124,6 +124,9 @@ protected:
     // Return true if succeed.
     virtual bool restoreFromTabInfo(const VEditTabInfo &p_info) = 0;
 
+    // Write modified buffer content to backup file.
+    virtual void writeBackupFile();
+
     // File related to this tab.
     QPointer<VFile> m_file;
 
@@ -146,6 +149,12 @@ protected:
     // File has diverged from disk.
     bool m_fileDiverged;
 
+    // Tab has been ready or not.
+    int m_ready;
+
+    // Whether backup file is enabled.
+    bool m_enableBackupFile;
+
 signals:
     void getFocused();
 
@@ -161,6 +170,9 @@ signals:
 
     void vimStatusUpdated(const VVim *p_vim);
 
+    // Request to close itself.
+    void closeRequested(VEditTab *p_tab);
+
 private slots:
     // Called when app focus changed.
     void handleFocusChanged(QWidget *p_old, QWidget *p_now);

+ 15 - 0
src/veditwindow.cpp

@@ -960,6 +960,8 @@ void VEditWindow::connectEditTab(const VEditTab *p_tab)
             this, &VEditWindow::handleTabStatusMessage);
     connect(p_tab, &VEditTab::vimStatusUpdated,
             this, &VEditWindow::handleTabVimStatusUpdated);
+    connect(p_tab, &VEditTab::closeRequested,
+            this, &VEditWindow::tabRequestToClose);
 }
 
 void VEditWindow::setCurrentWindow(bool p_current)
@@ -1094,3 +1096,16 @@ void VEditWindow::checkFileChangeOutside()
         getTab(i)->checkFileChangeOutside();
     }
 }
+
+void VEditWindow::tabRequestToClose(VEditTab *p_tab)
+{
+    bool ok = p_tab->closeFile(false);
+    if (ok) {
+        removeTab(indexOf(p_tab));
+
+        // Disconnect all the signals.
+        disconnect(p_tab, 0, this, 0);
+
+        p_tab->deleteLater();
+    }
+}

+ 3 - 0
src/veditwindow.h

@@ -138,6 +138,9 @@ private slots:
     // Handle the statusUpdated signal of VEditTab.
     void handleTabStatusUpdated(const VEditTabInfo &p_info);
 
+    // @p_tab request to close itself.
+    void tabRequestToClose(VEditTab *p_tab);
+
 private:
     void initTabActions();
     void setupCornerWidget();

+ 98 - 0
src/vfile.cpp

@@ -3,7 +3,15 @@
 #include <QDir>
 #include <QTextEdit>
 #include <QFileInfo>
+#include <QDebug>
+#include <QFile>
+#include <QTextStream>
 #include "utils/vutils.h"
+#include "vconfigmanager.h"
+
+extern VConfigManager *g_config;
+
+const QString VFile::c_backupFileHeadMagic = "vnote_backup_file_826537664";
 
 VFile::VFile(QObject *p_parent,
              const QString &p_name,
@@ -50,6 +58,11 @@ void VFile::close()
     }
 
     m_content.clear();
+    if (!m_backupName.isEmpty()) {
+        VUtils::deleteFile(fetchBackupFilePath());
+        m_backupName.clear();
+    }
+
     m_opened = false;
 }
 
@@ -57,6 +70,7 @@ bool VFile::save()
 {
     Q_ASSERT(m_opened);
     Q_ASSERT(m_modifiable);
+
     bool ret = VUtils::writeFileToDisk(fetchPath(), m_content);
     if (ret) {
         m_lastModified = QFileInfo(fetchPath()).lastModified();
@@ -110,3 +124,87 @@ void VFile::reload()
     m_content = VUtils::readFileFromDisk(filePath);
     m_lastModified = QFileInfo(filePath).lastModified();
 }
+
+QString VFile::backupFileOfPreviousSession() const
+{
+    Q_ASSERT(m_modifiable && m_backupName.isEmpty());
+
+    QString basePath = QDir(fetchBasePath()).filePath(g_config->getBackupDirectory());
+    QDir dir(basePath);
+
+    QStringList files = getPotentialBackupFiles(basePath);
+    foreach (const QString &file, files) {
+        QString filePath = dir.filePath(file);
+        if (isBackupFile(filePath)) {
+            return filePath;
+        }
+    }
+
+    return QString();
+}
+
+QString VFile::fetchBackupFilePath()
+{
+    QString basePath = QDir(fetchBasePath()).filePath(g_config->getBackupDirectory());
+    QDir dir(basePath);
+
+    if (m_backupName.isEmpty()) {
+        m_backupName = VUtils::getFileNameWithSequence(basePath,
+                                                       m_name + g_config->getBackupExtension(),
+                                                       true);
+
+        m_lastBackupFilePath = dir.filePath(m_backupName);
+    } else {
+        QString filePath = dir.filePath(m_backupName);
+        if (filePath != m_lastBackupFilePath) {
+            // File has been moved.
+            // Delete the original backup file if it still exists.
+            VUtils::deleteFile(m_lastBackupFilePath);
+
+            m_lastBackupFilePath = filePath;
+        }
+    }
+
+    return m_lastBackupFilePath;
+}
+
+QStringList VFile::getPotentialBackupFiles(const QString &p_dir) const
+{
+    QString nameFilter = QString("%1*%2").arg(m_name).arg(g_config->getBackupExtension());
+    QStringList files = QDir(p_dir).entryList(QStringList(nameFilter),
+                                              QDir::Files
+                                              | QDir::Hidden
+                                              | QDir::NoSymLinks
+                                              | QDir::NoDotAndDotDot);
+    return files;
+}
+
+bool VFile::isBackupFile(const QString &p_file) const
+{
+    QFile file(p_file);
+    if (!file.open(QFile::ReadOnly | QIODevice::Text)) {
+        return false;
+    }
+
+    QTextStream st(&file);
+    QString head = st.readLine();
+    return head == fetchBackupFileHead();
+}
+
+QString VFile::fetchBackupFileHead() const
+{
+    return c_backupFileHeadMagic + " " + fetchPath();
+}
+
+bool VFile::writeBackupFile(const QString &p_content)
+{
+    return VUtils::writeFileToDisk(fetchBackupFilePath(),
+                                   fetchBackupFileHead() + "\n" + p_content);
+}
+
+QString VFile::readBackupFile(const QString &p_file)
+{
+    const QString content = VUtils::readFileFromDisk(p_file);
+    int idx = content.indexOf("\n");
+    return content.mid(idx + 1);
+}

+ 27 - 0
src/vfile.h

@@ -79,6 +79,14 @@ public:
     // Whether this file was changed outside VNote.
     bool isChangedOutside() const;
 
+    // Return backup file of previous session if there exists one.
+    QString backupFileOfPreviousSession() const;
+
+    // Write @p_content to backup file.
+    bool writeBackupFile(const QString &p_content);
+
+    QString readBackupFile(const QString &p_file);
+
 protected:
     // Name of this file.
     QString m_name;
@@ -107,6 +115,25 @@ protected:
     // Last modified date and local time when the file is last modified
     // corresponding to m_content.
     QDateTime m_lastModified;
+
+    // Name of the backup file.
+    QString m_backupName;
+
+    // Used to identify file path change.
+    QString m_lastBackupFilePath;
+
+private:
+    // Fetch backup file path.
+    QString fetchBackupFilePath();
+
+    QStringList getPotentialBackupFiles(const QString &p_dir) const;
+
+    // Read the file content to check if it is a backup file.
+    bool isBackupFile(const QString &p_file) const;
+
+    QString fetchBackupFileHead() const;
+
+    static const QString c_backupFileHeadMagic;
 };
 
 inline const QString &VFile::getName() const

+ 24 - 7
src/vmdeditor.cpp

@@ -59,6 +59,11 @@ VMdEditor::VMdEditor(VFile *p_file,
     connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted,
             this, [this]() {
             makeBlockVisible(textCursor().block());
+
+            if (m_freshEdit) {
+                m_freshEdit = false;
+                emit m_object->ready();
+            }
     });
 
     m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter,
@@ -106,11 +111,6 @@ void VMdEditor::beginEdit()
     emit statusChanged();
 
     updateHeaders(m_mdHighlighter->getHeaderRegions());
-
-    if (m_freshEdit) {
-        m_freshEdit = false;
-        emit m_object->ready();
-    }
 }
 
 void VMdEditor::endEdit()
@@ -161,14 +161,14 @@ void VMdEditor::makeBlockVisible(const QTextBlock &p_block)
     }
 
     QScrollBar *vbar = verticalScrollBar();
-    if (!vbar || !vbar->isVisible()) {
+    if (!vbar || (vbar->minimum() == vbar->maximum())) {
         // No vertical scrollbar. No need to scroll.
         return;
     }
 
     int height = rect().height();
     QScrollBar *hbar = horizontalScrollBar();
-    if (hbar && hbar->isVisible()) {
+    if (hbar && (hbar->minimum() != hbar->maximum())) {
         height -= hbar->height();
     }
 
@@ -870,3 +870,20 @@ void VMdEditor::updateConfig()
     updateEditConfig();
     updateTextEditConfig();
 }
+
+QString VMdEditor::getContent() const
+{
+    return toPlainText();
+}
+
+void VMdEditor::setContent(const QString &p_content, bool p_modified)
+{
+    if (p_modified) {
+        QTextCursor cursor = textCursor();
+        cursor.select(QTextCursor::Document);
+        cursor.insertText(p_content);
+        setTextCursor(cursor);
+    } else {
+        setPlainText(p_content);
+    }
+}

+ 4 - 0
src/vmdeditor.h

@@ -55,6 +55,10 @@ public:
 
     void updateConfig() Q_DECL_OVERRIDE;
 
+    QString getContent() const Q_DECL_OVERRIDE;
+
+    void setContent(const QString &p_content, bool p_modified = false) Q_DECL_OVERRIDE;
+
 public slots:
     bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
 

+ 134 - 7
src/vmdtab.cpp

@@ -33,7 +33,8 @@ VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
       m_webViewer(NULL),
       m_document(NULL),
       m_mdConType(g_config->getMdConverterType()),
-      m_enableHeadingSequence(false)
+      m_enableHeadingSequence(false),
+      m_backupFileChecked(false)
 {
     V_ASSERT(m_file->getDocType() == DocType::Markdown);
 
@@ -49,6 +50,14 @@ VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
 
     setupUI();
 
+    m_backupTimer = new QTimer(this);
+    m_backupTimer->setSingleShot(true);
+    m_backupTimer->setInterval(g_config->getFileTimerInterval());
+    connect(m_backupTimer, &QTimer::timeout,
+            this, [this]() {
+                writeBackupFile();
+            });
+
     if (p_mode == OpenFileMode::Edit) {
         showFileEditMode();
     } else {
@@ -325,9 +334,13 @@ bool VMdTab::saveFile()
         m_editor->saveFile();
         ret = m_file->save();
         if (!ret) {
-            VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
+            VUtils::showMessage(QMessageBox::Warning,
+                                tr("Warning"),
+                                tr("Fail to save note."),
                                 tr("Fail to write to disk when saving a note. Please try it again."),
-                                QMessageBox::Ok, QMessageBox::Ok, this);
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
             m_editor->setModified(true);
         } else {
             m_fileDiverged = false;
@@ -376,8 +389,17 @@ void VMdTab::setupMarkdownViewer()
             this, SLOT(updateCurrentHeader(const QString &)));
     connect(m_document, &VDocument::keyPressed,
             this, &VMdTab::handleWebKeyPressed);
-    connect(m_document, SIGNAL(logicsFinished(void)),
-            this, SLOT(restoreFromTabInfo(void)));
+    connect(m_document, &VDocument::logicsFinished,
+            this, [this]() {
+                if (m_ready & TabReady::ReadMode) {
+                    return;
+                }
+
+                m_ready |= TabReady::ReadMode;
+
+                tabIsReady(TabReady::ReadMode);
+            });
+
     page->setWebChannel(channel);
 
     m_webViewer->setHtml(VUtils::generateHtmlTemplate(m_mdConType, false),
@@ -417,8 +439,16 @@ void VMdTab::setupMarkdownEditor()
             this, [this]() {
                 this->m_editArea->getFindReplaceDialog()->closeDialog();
             });
-    connect(m_editor->object(), SIGNAL(ready(void)),
-            this, SLOT(restoreFromTabInfo(void)));
+    connect(m_editor->object(), &VEditorObject::ready,
+            this, [this]() {
+                if (m_ready & TabReady::EditMode) {
+                    return;
+                }
+
+                m_ready |= TabReady::EditMode;
+
+                tabIsReady(TabReady::EditMode);
+            });
 
     enableHeadingSequence(m_enableHeadingSequence);
     m_editor->reloadFile();
@@ -842,3 +872,100 @@ void VMdTab::reload()
         showFileReadMode();
     }
 }
+
+void VMdTab::tabIsReady(TabReady p_mode)
+{
+    bool isCurrentMode = m_isEditMode && p_mode == TabReady::EditMode
+                         || !m_isEditMode && p_mode == TabReady::ReadMode;
+
+    if (isCurrentMode) {
+        restoreFromTabInfo();
+
+        if (m_enableBackupFile
+            && !m_backupFileChecked
+            && m_file->isModifiable()) {
+            if (!checkPreviousBackupFile()) {
+                return;
+            }
+        }
+    }
+
+    if (m_enableBackupFile
+        && m_file->isModifiable()
+        && p_mode == TabReady::EditMode) {
+        // contentsChanged will be emitted even the content is not changed.
+        connect(m_editor->document(), &QTextDocument::contentsChange,
+                this, [this]() {
+                    if (m_isEditMode) {
+                        m_backupTimer->stop();
+                        m_backupTimer->start();
+                    }
+                });
+    }
+}
+
+void VMdTab::writeBackupFile()
+{
+    Q_ASSERT(m_enableBackupFile && m_file->isModifiable());
+    m_file->writeBackupFile(m_editor->getContent());
+}
+
+bool VMdTab::checkPreviousBackupFile()
+{
+    m_backupFileChecked = true;
+
+    QString preFile = m_file->backupFileOfPreviousSession();
+    if (preFile.isEmpty()) {
+        return true;
+    }
+
+    QMessageBox box(QMessageBox::Warning,
+                    tr("Backup File Found"),
+                    tr("Found backup file <span style=\"%1\">%2</span> "
+                       "when opening note <span style=\"%1\">%3</span>.")
+                      .arg(g_config->c_dataTextStyle)
+                      .arg(preFile)
+                      .arg(m_file->fetchPath()),
+                    QMessageBox::NoButton,
+                    this);
+    QString backupContent = m_file->readBackupFile(preFile);
+    QString info = tr("VNote may crash while editing this note before.<br/>"
+                      "Please choose to recover from the backup file or delete it.<br/><br/>"
+                      "Note file last modified: <span style=\"%1\">%2</span><br/>"
+                      "Backup file last modified: <span style=\"%1\">%3</span><br/>"
+                      "Content comparison: <span style=\"%1\">%4</span>")
+                     .arg(g_config->c_dataTextStyle)
+                     .arg(VUtils::displayDateTime(QFileInfo(m_file->fetchPath()).lastModified()))
+                     .arg(VUtils::displayDateTime(QFileInfo(preFile).lastModified()))
+                     .arg(m_file->getContent() == backupContent ? tr("Identical")
+                                                                : tr("Different"));
+    box.setInformativeText(info);
+    QPushButton *recoverBtn = box.addButton(tr("Recover From Backup File"), QMessageBox::YesRole);
+    box.addButton(tr("Discard Backup File"), QMessageBox::NoRole);
+    QPushButton *cancelBtn = box.addButton(tr("Cancel"), QMessageBox::RejectRole);
+
+    box.setDefaultButton(cancelBtn);
+    box.setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+    box.exec();
+    QAbstractButton *btn = box.clickedButton();
+    if (btn == cancelBtn || !btn) {
+        // Close current tab.
+        emit closeRequested(this);
+        return false;
+    } else if (btn == recoverBtn) {
+        // Load content from the backup file.
+        if (!m_isEditMode) {
+            showFileEditMode();
+        }
+
+        Q_ASSERT(m_editor);
+        m_editor->setContent(backupContent, true);
+
+        updateStatus();
+    }
+
+    VUtils::deleteFile(preFile);
+
+    return true;
+}

+ 18 - 0
src/vmdtab.h

@@ -13,6 +13,7 @@ class QStackedLayout;
 class VDocument;
 class VMdEditor;
 class VInsertSelector;
+class QTimer;
 
 class VMdTab : public VEditTab
 {
@@ -88,6 +89,9 @@ public slots:
     // Enter edit mode.
     void editFile() Q_DECL_OVERRIDE;
 
+protected:
+    void writeBackupFile() Q_DECL_OVERRIDE;
+
 private slots:
     // Update m_outline according to @p_tocHtml for read mode.
     void updateOutlineFromHtml(const QString &p_tocHtml);
@@ -115,6 +119,8 @@ private slots:
     void restoreFromTabInfo();
 
 private:
+    enum TabReady { None = 0, ReadMode = 0x1, EditMode = 0x2 };
+
     // Setup UI.
     void setupUI();
 
@@ -166,6 +172,13 @@ private:
     // Prepare insert selector with snippets.
     VInsertSelector *prepareSnippetSelector(QWidget *p_parent = nullptr);
 
+    // Called once read or edit mode is ready.
+    void tabIsReady(TabReady p_mode);
+
+    // Check if there exists backup file from previous session.
+    // Return true if we could continue.
+    bool checkPreviousBackupFile();
+
     VMdEditor *m_editor;
     VWebView *m_webViewer;
     VDocument *m_document;
@@ -175,6 +188,11 @@ private:
     bool m_enableHeadingSequence;
 
     QStackedLayout *m_stacks;
+
+    // Timer to write backup file when content has been changed.
+    QTimer *m_backupTimer;
+
+    bool m_backupFileChecked;
 };
 
 inline VMdEditor *VMdTab::getEditor()