From 3a25e7332bce6e223b39549f7f5a52668bd1a911 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 13 Feb 2020 19:03:21 +0100 Subject: [PATCH 1/5] Change the way the project file is saved --- nw/guimain.py | 6 ++-- nw/project/project.py | 65 +++++++++++++------------------------------ 2 files changed, 22 insertions(+), 49 deletions(-) diff --git a/nw/guimain.py b/nw/guimain.py index a1b88781..b1fdf031 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -330,7 +330,7 @@ class GuiMain(QMainWindow): return True - def saveProject(self, isAuto=False): + def saveProject(self): """Save the current project. """ if not self.hasProject: @@ -344,7 +344,7 @@ class GuiMain(QMainWindow): return False self.treeView.saveTreeOrder() - self.theProject.saveProject(isAuto) + self.theProject.saveProject() self.theIndex.saveIndex() self.mainMenu.updateRecentProjects() @@ -840,7 +840,7 @@ class GuiMain(QMainWindow): if (self.hasProject and self.theProject.projChanged and self.theProject.projPath is not None): logger.debug("Autosaving project") - self.saveProject(isAuto=True) + self.saveProject() return def _autoSaveDocument(self): diff --git a/nw/project/project.py b/nw/project/project.py index e6167eb9..a8960de2 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -13,7 +13,7 @@ import logging import nw -from os import path, mkdir, listdir, unlink +from os import path, mkdir, listdir, unlink, rename from shutil import copyfile from lxml import etree from hashlib import sha256 @@ -180,6 +180,11 @@ class NWProject(): return def openProject(self, fileName): + """Open the project file provided, or if doesn't exist, assume + it is a folder, and look for the file within it. If successful, + parse the XML of the file and populate the project variables and + build the tree of project items. + """ if not path.isfile(fileName): fileName = path.join(fileName, nwFiles.PROJ_FILE) @@ -288,7 +293,7 @@ class NWProject(): return True - def saveProject(self, isAuto=False): + def saveProject(self): if self.projPath is None: self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR) @@ -303,10 +308,6 @@ class NWProject(): logger.debug("Saving project: %s" % self.projPath) - # Save a copy of the current file, just in case - if not isAuto: - self._maintainPrevious() - # Root element and project details logger.debug("Writing project meta") nwXML = etree.Element("novelWriterXML",attrib={ @@ -345,9 +346,11 @@ class NWProject(): self.projTree[tHandle].packXML(xContent) # Write the xml tree to file - saveFile = path.join(self.projPath,self.projFile) + tempFile = path.join(self.projPath, self.projFile+"~") + saveFile = path.join(self.projPath, self.projFile) + backFile = path.join(self.projPath, self.projFile[:-3]+"bak") try: - with open(saveFile,mode="wb") as outFile: + with open(tempFile, mode="wb") as outFile: outFile.write(etree.tostring( nwXML, pretty_print = True, @@ -358,6 +361,14 @@ class NWProject(): self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR) return False + # If we're here, the file was successfully saved, + # so let's sort out the temps and backups + if path.isfile(backFile): + unlink(backFile) + if path.isfile(saveFile): + rename(saveFile, backFile) + rename(tempFile, saveFile) + self.mainConf.setRecent(self.projPath) self.theParent.setStatus("Saved Project: %s" % self.projName) self.setProjectChanged(False) @@ -740,42 +751,4 @@ class NWProject(): itemHandle = self._makeHandle(addSeed+"!") return itemHandle - def _maintainPrevious(self): - """This function will take the current project file and copy it - into the project cache folder with an incremental file extension - added. These serve as a backup in case the xml file gets - corrupted. - """ - - countFile = path.join(self.projCache, nwFiles.PROJ_COUNT) - projCount = 0 - - if path.isfile(countFile): - try: - with open(countFile, mode="r") as inFile: - projCount = int(inFile.read())+1 - except: - projCount = 0 - - if projCount > 9: - projCount = 0 - - projBackup = "%s.%d" % (nwFiles.PROJ_FILE, projCount) - - try: - copyfile( - path.join(self.projPath, self.projFile), - path.join(self.projCache, projBackup) - ) - except: - logger.error("Failed to write to file %s" % projBackup) - - try: - with open(countFile, mode="w") as outFile: - outFile.write(str(projCount)) - except: - logger.error("Failed to write to file %s" % countFile) - - return - # END Class NWProject From ca7617ad075444778b74d279a4bdc081ca4cae79 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 13 Feb 2020 19:07:51 +0100 Subject: [PATCH 2/5] Remove the project cache folder as it isn't, and was never really, needed --- nw/constants/constants.py | 1 - nw/project/project.py | 17 +++++------------ tests/test_gui.py | 2 -- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index d3ccd1c7..f85f6558 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -22,7 +22,6 @@ class nwFiles(): APP_ICON = "novelWriter.svg" PROJ_FILE = "nwProject.nwx" - PROJ_COUNT = "projCount.txt" PROJ_DICT = "wordlist.txt" SESS_INFO = "sessionInfo.log" INDEX_FILE = "tagsIndex.json" diff --git a/nw/project/project.py b/nw/project/project.py index a8960de2..60cdfa72 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -50,7 +50,6 @@ class NWProject(): self.trashRoot = None # The handle of the trash root folder self.projPath = None # The full path to where the currently open project is saved self.projMeta = None # The full path to the project's meta data folder - self.projCache = None # The full path to the project's cache folder self.projDict = None # The spell check dictionary self.projFile = None # The file name of the project main xml file @@ -154,7 +153,6 @@ class NWProject(): self.trashRoot = None self.projPath = None self.projMeta = None - self.projCache = None self.projDict = None self.projFile = nwFiles.PROJ_FILE self.projName = "" @@ -196,14 +194,11 @@ class NWProject(): self.projPath = path.dirname(fileName) logger.debug("Opening project: %s" % self.projPath) - self.projMeta = path.join(self.projPath,"meta") - self.projCache = path.join(self.projPath,"cache") - self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) + self.projMeta = path.join(self.projPath,"meta") + self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) if not self._checkFolder(self.projMeta): return - if not self._checkFolder(self.projCache): - return try: nwXML = etree.parse(fileName) @@ -299,12 +294,10 @@ class NWProject(): self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR) return False - self.projMeta = path.join(self.projPath,"meta") - self.projCache = path.join(self.projPath,"cache") + self.projMeta = path.join(self.projPath,"meta") - if not self._checkFolder(self.projPath): return - if not self._checkFolder(self.projMeta): return - if not self._checkFolder(self.projCache): return + if not self._checkFolder(self.projPath): return + if not self._checkFolder(self.projMeta): return logger.debug("Saving project: %s" % self.projPath) diff --git a/tests/test_gui.py b/tests/test_gui.py index 89121ace..dc279222 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -37,7 +37,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): assert nwGUI.theProject.trashRoot is None assert nwGUI.theProject.projPath is None assert nwGUI.theProject.projMeta is None - assert nwGUI.theProject.projCache is None assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projName == "" assert nwGUI.theProject.bookTitle == "" @@ -62,7 +61,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): assert nwGUI.theProject.trashRoot is None assert nwGUI.theProject.projPath == nwTempGUI assert nwGUI.theProject.projMeta == path.join(nwTempGUI,"meta") - assert nwGUI.theProject.projCache == path.join(nwTempGUI,"cache") assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projName == "" assert nwGUI.theProject.bookTitle == "" From c28a246d238402c9196013a2254ae2eb227526bd Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 13 Feb 2020 19:16:29 +0100 Subject: [PATCH 3/5] Make document saving consistent with how project file is saved --- nw/project/document.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/nw/project/document.py b/nw/project/document.py index e2a46d65..9339a4cd 100644 --- a/nw/project/document.py +++ b/nw/project/document.py @@ -100,25 +100,23 @@ class NWDoc(): mkdir(dataPath) logger.debug("Created folder %s" % dataPath) - docTemp = path.join(dataPath,docFile[:-3]+"tmp") - docBack = path.join(dataPath,docFile[:-3]+"bak") - - if path.isfile(docTemp): - unlink(docTemp) - if path.isfile(docBack): - rename(docBack,docTemp) - if path.isfile(docPath): - rename(docPath,docBack) + docTemp = path.join(dataPath, docFile+"~") + docBack = path.join(dataPath, docFile[:-3]+"bak") try: - with open(docPath,mode="w",encoding="utf8") as outFile: + with open(docTemp,mode="w",encoding="utf8") as outFile: outFile.write(docText) except Exception as e: self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR) return False - if path.isfile(docTemp): - unlink(docTemp) + # If we're here, the file was successfully saved, + # so let's sort out the temps and backups + if path.isfile(docBack): + unlink(docBack) + if path.isfile(docPath): + rename(docPath, docBack) + rename(docTemp, docPath) self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName) @@ -132,8 +130,8 @@ class NWDoc(): dataPath = path.join(self.theProject.projPath, docDir) chkList = [] chkList.append(path.join(dataPath, docFile)) - chkList.append(path.join(dataPath,docFile[:-3]+"tmp")) - chkList.append(path.join(dataPath,docFile[:-3]+"bak")) + chkList.append(path.join(dataPath, docFile+"~")) + chkList.append(path.join(dataPath, docFile[:-3]+"bak")) for chkFile in chkList: if path.isfile(chkFile): try: @@ -147,7 +145,7 @@ class NWDoc(): @staticmethod def assemblePath(tHandle, docExt): if tHandle is None: - return None + return None, None docDir = "data_"+tHandle[0] docFile = tHandle[1:13]+"_"+docExt return docDir, docFile From 760aee0c8f5d1be3a8063522f1decf9e390df2a9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 13 Feb 2020 21:12:28 +0100 Subject: [PATCH 4/5] Added a function to clean out old project files no longer needed --- nw/project/project.py | 6 ++++++ nw/tools/__init__.py | 2 ++ nw/tools/legacy.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 nw/tools/legacy.py diff --git a/nw/project/project.py b/nw/project/project.py index 60cdfa72..03aacf42 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -22,6 +22,7 @@ from time import time from nw.project.status import NWStatus from nw.project.item import NWItem +from nw.tools import projectMaintenance from nw.common import checkString, checkBool, checkInt from nw.constants import ( nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert @@ -200,6 +201,11 @@ class NWProject(): if not self._checkFolder(self.projMeta): return + try: + projectMaintenance(self) + except Exception as E: + logger.error(str(E)) + try: nwXML = etree.parse(fileName) except Exception as e: diff --git a/nw/tools/__init__.py b/nw/tools/__init__.py index 582d18fb..71f6b141 100644 --- a/nw/tools/__init__.py +++ b/nw/tools/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from nw.tools.analyse import TextAnalysis +from nw.tools.legacy import projectMaintenance from nw.tools.optlaststate import OptLastState from nw.tools.spellcheck import NWSpellCheck from nw.tools.spellenchant import NWSpellEnchant @@ -10,6 +11,7 @@ from nw.tools.wordcount import countWords __all__ = [ "TextAnalysis", + "projectMaintenance", "OptLastState", "NWSpellCheck", "NWSpellEnchant", diff --git a/nw/tools/legacy.py b/nw/tools/legacy.py new file mode 100644 index 00000000..7b622cb6 --- /dev/null +++ b/nw/tools/legacy.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +"""novelWriter Legacy Tools + + novelWriter – Legacy Tools +============================ + Various functions to handle old projects + + File History: + Created: 2020-02-13 [0.4.3] + +""" + +import logging +import nw + +from os import path, unlink, rmdir + +logger = logging.getLogger(__name__) + +def projectMaintenance(theProject): + """Wrapper class for handling various tasks related to managing old + projects with content from older versions of novelWriter. + """ + + # Remove no longer used project cache folder + if path.isdir(theProject.projPath): + cacheDir = path.join(theProject.projPath, "cache") + if path.isdir(cacheDir): + logger.info("Deprecated cache folder found") + rmList = [] + for i in range(10): + rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i)) + rmList.append(path.join(cacheDir, "projCount.txt")) + for rmFile in rmList: + if path.isfile(rmFile): + logger.info("Deleting: %s" % rmFile) + try: + unlink(rmFile) + except Exception as e: + logger.error(str(e)) + logger.info("Deleting: %s" % cacheDir) + try: + rmdir(cacheDir) + except Exception as e: + logger.error(str(e)) + + return From beb656267cec976402295d2eed0dd44d645c3908 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 13 Feb 2020 21:22:36 +0100 Subject: [PATCH 5/5] If opening a project file fails, try to open the backup file instead --- nw/project/project.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/nw/project/project.py b/nw/project/project.py index 03aacf42..c564a1f6 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -210,8 +210,20 @@ class NWProject(): nwXML = etree.parse(fileName) except Exception as e: self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR) - self.clearProject() - return False + + # Trying to open backup file instead + backFile = fileName[:-3]+"bak" + if path.isfile(backFile): + self.makeAlert("Attempting to open backup project file instead.", nwAlert.INFO) + try: + nwXML = etree.parse(backFile) + except Exception as e: + self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR) + self.clearProject() + return False + else: + self.clearProject() + return False xRoot = nwXML.getroot() nwxRoot = xRoot.tag @@ -295,6 +307,11 @@ class NWProject(): return True def saveProject(self): + """Save the project main XML file. The saving command itself + uses a temporary filename, and the file is renamed afterwards to + make sure if the save fails, we're not left with a truncated + file. + """ if self.projPath is None: self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)