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/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/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 diff --git a/nw/project/project.py b/nw/project/project.py index e6167eb9..c564a1f6 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 @@ -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 @@ -50,7 +51,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 +154,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 = "" @@ -180,6 +179,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) @@ -191,21 +195,35 @@ 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: + projectMaintenance(self) + except Exception as E: + logger.error(str(E)) try: 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 @@ -288,25 +306,24 @@ class NWProject(): return True - def saveProject(self, isAuto=False): + 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) 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) - # 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 +362,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 +377,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 +767,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 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 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 == ""