diff --git a/nw/constants/constants.py b/nw/constants/constants.py index bc20af31..cf0ab4eb 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -39,6 +39,8 @@ class nwFiles(): PROJ_FILE = "nwProject.nwx" PROJ_DICT = "wordlist.txt" PROJ_LOCK = "nwProject.lock" + TOC_TXT = "ToC.txt" + TOC_JSON = "ToC.json" SESS_INFO = "sessionInfo.log" INDEX_FILE = "tagsIndex.json" OPTS_FILE = "guiOptions.json" diff --git a/nw/core/__init__.py b/nw/core/__init__.py index 6dd65fd2..0107dac2 100644 --- a/nw/core/__init__.py +++ b/nw/core/__init__.py @@ -9,7 +9,6 @@ from nw.core.spellcheck import NWSpellSimple from nw.core.tokenizer import Tokenizer from nw.core.tohtml import ToHtml from nw.core.tools import countWords -from nw.core.tools import projectMaintenance from nw.core.tools import numberToWord __all__ = [ @@ -22,6 +21,5 @@ __all__ = [ "Tokenizer", "ToHtml", "countWords", - "projectMaintenance", "numberToWord", ] diff --git a/nw/core/project.py b/nw/core/project.py index cf35f629..c89bb645 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -42,7 +42,6 @@ from shutil import make_archive from PyQt5.QtWidgets import QMessageBox from nw.gui.tools import OptionState -from nw.core.tools import projectMaintenance from nw.core.document import NWDoc from nw.common import checkString, checkBool, checkInt, formatTimeStamp from nw.constants import ( @@ -72,11 +71,12 @@ class NWProject(): self.autoCount = 0 # Meta data: number of automatic saves # Class Settings - 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.projData = None # The full path to the project's data folder - self.projDict = None # The spell check dictionary - self.projFile = None # The file name of the project main XML file + 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.projContent = None # The full path to the project's content folder + self.projDict = None # The spell check dictionary + self.projFile = None # The file name of the project main XML file # Project Meta self.projName = "" # Project name (working title) @@ -196,7 +196,8 @@ class NWProject(): # Project Settings self.projPath = None self.projMeta = None - self.projData = None + self.projCache = None + self.projContent = None self.projDict = None self.projFile = nwFiles.PROJ_FILE self.projName = "" @@ -248,14 +249,37 @@ class NWProject(): self.projPath = path.abspath(path.dirname(fileName)) logger.debug("Opening project: %s" % self.projPath) - self.projMeta = path.join(self.projPath,"meta") - self.projData = path.join(self.projPath,"content") - self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) + # Standard Folders and Files + # ========================== + + self.projMeta = path.join(self.projPath, "meta") + self.projCache = path.join(self.projPath, "cache") + self.projContent = path.join(self.projPath, "content") + self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) if not self._checkFolder(self.projMeta): return False - if not self._checkFolder(self.projData): + if not self._checkFolder(self.projCache): return False + if not self._checkFolder(self.projContent): + return False + + # Check for Old Legacy Data + # ========================= + + errList = [] + for projItem in listdir(self.projPath): + logger.verbose("Project contains: %s" % projItem) + if projItem.startswith("data_"): + self._legacyDataFolder(projItem) + + if errList: + self.makeAlert(errList, nwAlert.ERROR) + + self._deprecatedFiles() + + # Project Lock + # ============ if overrideLock: self._clearLockFile() @@ -272,10 +296,8 @@ class NWProject(): else: logger.verbose("Project is not locked") - try: - projectMaintenance(self) - except Exception as E: - logger.error(str(E)) + # Open The Project XML File + # ========================= try: nwXML = etree.parse(fileName) @@ -321,6 +343,7 @@ class NWProject(): # Check File Type # =============== + if not nwxRoot == "novelWriterXML": self.makeAlert( "Project file does not appear to be a novelWriterXML file.", @@ -330,14 +353,17 @@ class NWProject(): # Check Project Storage Version # ============================= + if fileVersion == "1.0": msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Old Project Version", ( "The project file and data is created by a %s version lower than 0.7. " "Do you want to upgrade the project to the most recent format?

" "Note that after the upgrade, you cannot open the project with an older " - "version of novelWriter any more, so make sure you have a recent backup." - ) % nw.__package__) + "version of %s any more, so make sure you have a recent backup." + ) % ( + nw.__package__, nw.__package__ + )) if msgRes == QMessageBox.Yes: self._updateStorage() else: @@ -353,6 +379,7 @@ class NWProject(): # Check novelWriter Version # ========================= + if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Version Conflict", ( @@ -367,6 +394,7 @@ class NWProject(): # Start Parsing XML # ================= + for xChild in xRoot: if xChild.tag == "project": logger.debug("Found project meta") @@ -384,6 +412,7 @@ class NWProject(): self.bookAuthors.append(xItem.text) elif xItem.tag == "backup": self.doBackup = checkBool(xItem.text, False) + elif xChild.tag == "settings": logger.debug("Found project settings") for xItem in xChild: @@ -411,6 +440,7 @@ class NWProject(): for xEntry in xItem: titleFormat[xEntry.tag] = checkString(xEntry.text, "", False) self.setTitleFormat(titleFormat) + elif xChild.tag == "content": logger.debug("Found project content") self.projTree.unpackXML(xChild) @@ -444,14 +474,14 @@ class NWProject(): return False self.projMeta = path.join(self.projPath, "meta") - self.projData = path.join(self.projPath, "content") + self.projContent = path.join(self.projPath, "content") saveTime = time() if not self._checkFolder(self.projPath): return False if not self._checkFolder(self.projMeta): return False - if not self._checkFolder(self.projData): + if not self._checkFolder(self.projContent): return False logger.debug("Saving project: %s" % self.projPath) @@ -978,7 +1008,7 @@ class NWProject(): # Then check the files in the data folder orphanFiles = [] - for fileItem in listdir(self.projData): + for fileItem in listdir(self.projContent): if not fileItem.endswith(".nwd"): logger.warning("Skipping file %s" % fileItem) continue @@ -1046,52 +1076,113 @@ class NWProject(): return True - def _updateStorage(self): - """Updates the project storage folder from 1.0 to 1.1. + ## + # Legacy Data Structure Handlers + ## + + def _legacyDataFolder(self, theFolder): + """Clean up legacy data folders. """ - contDir = path.join(self.projPath, "content") - self._checkFolder(contDir) errList = [] + theData = path.join(self.projPath, theFolder) + if not path.isdir(theData): + errList.append("Not a folder: %s" % theData) + return errList - for projItem in listdir(self.projPath): - itemPath = path.join(self.projPath, projItem) - if not path.isdir(itemPath) or not projItem.startswith("data_"): + logger.info("Old data folder %s found" % theFolder) + + # Move Documents to Content + # ========================= + for dataItem in listdir(theData): + theFile = path.join(theData, dataItem) + if not path.isfile(theFile): + theErr = self._moveUnknownItem(theData, dataItem) + if theErr: + errList.append(theErr) continue - for dataFile in listdir(itemPath): - dataPath = path.join(itemPath, dataFile) - if dataFile.endswith(".bak"): - try: - unlink(dataPath) - logger.info("Deleted file: %s" % dataPath) - except: - errList.append("Failed to delete: %s" % dataPath) - elif dataFile.endswith(".nwd") and len(dataFile) == 21: - tHandle = projItem[-1]+dataFile[:12] - newPath = path.join(contDir, tHandle+".nwd") - try: - rename(dataPath, newPath) - logger.info("Moved file: %s" % dataPath) - logger.info("New location: %s" % newPath) - except: - errList.append("Failed to move: %s" % dataPath) + if len(dataItem) == 21 and dataItem.endswith("_main.nwd"): + tHandle = theFolder[-1]+dataItem[:12] + newPath = path.join(self.projContent, tHandle+".nwd") + try: + rename(theFile, newPath) + logger.info("Moved file: %s" % theFile) + logger.info("New location: %s" % newPath) + except Exception as e: + logger.error(str(e)) + errList.append("Could not move: %s" % theFile) - else: - newPath = path.join(self.projPath, "unknown_"+dataFile) - try: - rename(dataPath, newPath) - logger.info("Moved file: %s" % dataPath) - logger.info("New location: %s" % newPath) - except: - errList.append("Failed to move: %s" % dataPath) - try: - rmdir(itemPath) - logger.info("Removed folder: %s" % itemPath) - except: - errList.append("Failed to delete: %s" % itemPath) + elif len(dataItem) == 21 and dataItem.endswith("_main.bak"): + try: + unlink(theFile) + logger.info("Deleted file: %s" % theFile) + except Exception as e: + logger.error(str(e)) + errList.append("Could not delete: %s" % theFile) - if errList: - self.makeAlert(errList, nwAlert.ERROR) + else: + theErr = self._moveUnknownItem(theData, dataItem) + if theErr: + errList.append(theErr) + + # Remove Data Folder + # ================== + try: + rmdir(theData) + logger.info("Removed folder: %s" % theFolder) + except: + errList.append("Failed to remove: %s" % theFolder) + + return errList + + def _moveUnknownItem(self, theDir, theItem): + """Move an item that doesn't belong in the project folder to + a junk folder. + """ + theJunk = path.join(self.projPath, "junk") + if not self._checkFolder(theJunk): + return "Could not make folder: %s" % theJunk + + theSrc = path.join(theDir, theItem) + theDst = path.join(theJunk, theItem) + + try: + rename(theSrc, theDst) + logger.info("Moved to junk: %s" % theSrc) + except Exception as e: + logger.error(str(e)) + return "Could not move item %s to junk." % theSrc + + return "" + + def _deprecatedFiles(self): + """Delete files that are no longer used by novelWriter. + """ + rmList = [] + rmList.append(path.join(self.projCache, "nwProject.nwx.0")) + rmList.append(path.join(self.projCache, "nwProject.nwx.1")) + rmList.append(path.join(self.projCache, "nwProject.nwx.2")) + rmList.append(path.join(self.projCache, "nwProject.nwx.3")) + rmList.append(path.join(self.projCache, "nwProject.nwx.4")) + rmList.append(path.join(self.projCache, "nwProject.nwx.5")) + rmList.append(path.join(self.projCache, "nwProject.nwx.6")) + rmList.append(path.join(self.projCache, "nwProject.nwx.7")) + rmList.append(path.join(self.projCache, "nwProject.nwx.8")) + rmList.append(path.join(self.projCache, "nwProject.nwx.9")) + rmList.append(path.join(self.projMeta, "mainOptions.json")) + rmList.append(path.join(self.projMeta, "exportOptions.json")) + rmList.append(path.join(self.projMeta, "outlineOptions.json")) + rmList.append(path.join(self.projMeta, "timelineOptions.json")) + rmList.append(path.join(self.projMeta, "docMergeOptions.json")) + rmList.append(path.join(self.projMeta, "sessionLogOptions.json")) + + for rmFile in rmList: + if path.isfile(rmFile): + logger.info("Deleting: %s" % rmFile) + try: + unlink(rmFile) + except Exception as e: + logger.error(str(e)) return diff --git a/nw/core/tools.py b/nw/core/tools.py index 8aa81d7b..8115105d 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -8,7 +8,6 @@ File History: Created: 2019-04-22 [0.0.1] countWords Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN - Created: 2020-02-13 [0.4.3] projectMaintenance Merged: 2020-05-08 [0.4.5] All of the above into this file This file is a part of novelWriter @@ -81,45 +80,6 @@ def countWords(theText): return charCount, wordCount, paraCount -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 content 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)) - - # Remove no longer used meta files - rmList = [] - rmList.append(path.join(theProject.projMeta, "mainOptions.json")) - rmList.append(path.join(theProject.projMeta, "exportOptions.json")) - rmList.append(path.join(theProject.projMeta, "outlineOptions.json")) - rmList.append(path.join(theProject.projMeta, "timelineOptions.json")) - rmList.append(path.join(theProject.projMeta, "docMergeOptions.json")) - rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json")) - for rmFile in rmList: - if path.isfile(rmFile): - logger.info("Deleting: %s" % rmFile) - try: - unlink(rmFile) - except Exception as e: - logger.error(str(e)) - - return - def numberToWord(numVal, theLanguage): """Wrapper for converting numbers to words for chapter headings. """ diff --git a/sample/content/ae7339df26ded.nwd b/sample/content/ae7339df26ded.nwd index 8b92a69c..05ed60d5 100644 --- a/sample/content/ae7339df26ded.nwd +++ b/sample/content/ae7339df26ded.nwd @@ -2,6 +2,6 @@ ### We Found John! @pov: John -@location: Mars, OuterSpace +@location: Mars Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes. diff --git a/sample/content/b8136a5a774a0.nwd b/sample/content/b8136a5a774a0.nwd index a304cb35..4e63373d 100644 --- a/sample/content/b8136a5a774a0.nwd +++ b/sample/content/b8136a5a774a0.nwd @@ -1,4 +1,4 @@ -%%~ b8136a5a774a0:7031beac91f75:Delete Me! +%%~ b8136a5a774a0:98acd8c76c93a:Delete Me! ### Delete Me! This scene is trash. \ No newline at end of file diff --git a/sample/content/edca4be2fcaf8.nwd b/sample/content/edca4be2fcaf8.nwd index 5fef1f34..679e4e27 100644 --- a/sample/content/edca4be2fcaf8.nwd +++ b/sample/content/edca4be2fcaf8.nwd @@ -1,4 +1,4 @@ -%%~ edca4be2fcaf8:7031beac91f75:Part One +%%~ edca4be2fcaf8:7031beac91f75:Part 1 # Part One The first part. \ No newline at end of file diff --git a/sample/content/f1471bef9f2ae.nwd b/sample/content/f1471bef9f2ae.nwd index 3bd03e9d..9b2d9368 100644 --- a/sample/content/f1471bef9f2ae.nwd +++ b/sample/content/f1471bef9f2ae.nwd @@ -5,10 +5,4 @@ Space … it’s an awful lot of nothing, with bits in it here and there. Some of which, people like to call home. -## Outer Space -@tag: OuterSpace - -Now even further into space! - -You can have more than one tag in a file, as long as there is only one tag per heading. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index b6f63962..58b97959 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -11,8 +11,8 @@ True True 636b6aa9b697b - bc0cbd2a407f3 - 941 + ba8a28a246524 + 914 B E @@ -73,7 +73,7 @@ False True PAGE - 210 + 208 40 2 213 @@ -89,7 +89,7 @@ 23 5 1 - 27 + 0 A Folder @@ -122,7 +122,7 @@ 1199 216 7 - 1066 + 1266 Another Scene @@ -135,7 +135,7 @@ 476 93 3 - 428 + 551 Interlude @@ -148,7 +148,7 @@ 633 101 3 - 752 + 1238 A Note on Structure @@ -161,7 +161,7 @@ 1692 313 6 - 551 + 1721 Chapter Two @@ -174,7 +174,7 @@ 139 28 1 - 242 + 343 We Found John! @@ -214,7 +214,7 @@ 49 9 1 - 65 + 24 Jane Smith @@ -227,7 +227,7 @@ 55 9 1 - 71 + 25 Locations @@ -247,7 +247,7 @@ 76 15 1 - 93 + 20 Space @@ -257,10 +257,10 @@ False True NOTE - 241 - 51 - 3 - 135 + 115 + 24 + 1 + 133 Mars