diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 750e68db..709310a2 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -105,20 +105,24 @@ class NWProject(QObject): ## @property - def data(self): - return self._data + def options(self): + return self._options @property - def index(self): - return self._index + def storage(self): + return self._storage + + @property + def data(self): + return self._data @property def tree(self): return self._tree @property - def options(self): - return self._options + def index(self): + return self._index @property def projOpened(self): @@ -245,6 +249,7 @@ class NWProject(QObject): self._projAltered = False # Project Tree + self._storage.clear() self._tree.clear() self._index.clearIndex() self._data = NWProjectData(self) @@ -302,6 +307,8 @@ class NWProject(QObject): if not self.setProjectPath(projPath, newProject=True): return False + self._storage.openProjectInPlace(self.projPath) + self._data.setName(projName) self._data.setTitle(projTitle) self._data.setAuthors(projAuthors) @@ -458,10 +465,18 @@ class NWProject(QObject): # Open The Project XML File # ========================= + if not self._storage.openProjectInPlace(self.projPath): + self.clearProject() + return False + + xmlReader = self._storage.getXmlReader() + if not isinstance(xmlReader, ProjectXMLReader): + self.clearProject() + return False + self._data = NWProjectData(self) projContent = [] - xmlReader = ProjectXMLReader(fileName) xmlParsed = xmlReader.read(self._data, projContent) appVersion = xmlReader.appVersion or self.tr("Unknown") @@ -580,6 +595,12 @@ class NWProject(QObject): ), nwAlert.ERROR) return False + if not self._storage.isOpen(): + self.mainGui.makeAlert(self.tr( + "There is no project open." + ), nwAlert.ERROR) + return False + saveTime = time() if not self.ensureFolderStructure(): return False @@ -594,11 +615,13 @@ class NWProject(QObject): self.updateWordCounts() self.countStatus() + xmlWriter = self._storage.getXmlWriter() + if not isinstance(xmlWriter, ProjectXMLWriter): + return False + saveTime = time() editTime = int(self._data.editTime + saveTime - self._projOpened) - content = self._tree.pack() - xmlWriter = ProjectXMLWriter(self.projPath) if not xmlWriter.write(self._data, content, saveTime, editTime): self.mainGui.makeAlert(self.tr( "Failed to save project." diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 18020c9a..1cfe770f 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -31,6 +31,7 @@ import novelwriter from enum import Enum from lxml import etree from time import time +from pathlib import Path from novelwriter.common import ( checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString @@ -91,7 +92,7 @@ class ProjectXMLReader: def __init__(self, path): - self._path = path + self._path = Path(path) self._state = XMLReadState.NO_ACTION self._root = "" @@ -153,7 +154,7 @@ class ProjectXMLReader: logger.debug("Reading project XML") try: - xml = etree.parse(self._path) + xml = etree.parse(str(self._path)) self._state = XMLReadState.NO_ERROR except Exception as exc: @@ -161,10 +162,10 @@ class ProjectXMLReader: logger.error("Failed to parse project XML", exc_info=exc) self._state = XMLReadState.CANNOT_PARSE - backFile = self._path[:-3]+"bak" + backFile = self._path.with_suffix(".bak") if os.path.isfile(backFile): try: - xml = etree.parse(backFile) + xml = etree.parse(str(backFile)) self._state = XMLReadState.PARSED_BACKUP logger.info("Backup project file parsed") except Exception as exc: @@ -445,7 +446,7 @@ class ProjectXMLWriter: def __init__(self, path): - self._path = path + self._path = Path(path) self._error = None return @@ -514,17 +515,13 @@ class ProjectXMLWriter: xName.text = item["name"] # Write the XML tree to file - saveFile = os.path.join(self._path, nwFiles.PROJ_FILE) - tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~") - backFile = os.path.join(self._path, nwFiles.PROJ_FILE[:-3]+"bak") + saveFile = self._path / nwFiles.PROJ_FILE + tempFile = saveFile.with_suffix(".tmp") + backFile = saveFile.with_suffix(".bak") try: - with open(tempFile, mode="wb") as outFile: - outFile.write(etree.tostring( - xRoot, - pretty_print=True, - encoding="utf-8", - xml_declaration=True - )) + tempFile.write_bytes(etree.tostring( + xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True + )) except Exception as exc: self._error = exc return False @@ -532,10 +529,10 @@ class ProjectXMLWriter: # If we're here, the file was successfully saved, # so let's sort out the temps and backups try: - if os.path.isfile(saveFile): - os.replace(saveFile, backFile) - os.replace(tempFile, saveFile) - except OSError as exc: + if saveFile.exists(): + saveFile.replace(backFile) + tempFile.replace(saveFile) + except Exception as exc: self._error = exc return False diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py index 7382c336..43e5d618 100644 --- a/novelwriter/core/storage.py +++ b/novelwriter/core/storage.py @@ -25,37 +25,109 @@ along with this program. If not, see . import logging +from pathlib import Path + +from novelwriter.constants import nwFiles +from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter + logger = logging.getLogger(__name__) class NWStorage: + MODE_INACTIVE = 0 + MODE_INPLACE = 1 + MODE_ARCHIVE = 2 + def __init__(self, theProject): + self.theProject = theProject + + self._storagePath = None + self._runtimePath = None + self._openMode = self.MODE_INACTIVE + + return + + def clear(self): + """Reset internal variables. + """ + self._storagePath = None + self._runtimePath = None + self._openMode = self.MODE_INACTIVE return ## # Core Methods ## - def openProjectFolder(self, path): - pass + def isOpen(self): + """Check if the storage location is open. + """ + return self._runtimePath is not None + + def openProjectInPlace(self, path): + """Open a novelWriter project in-place. That is, it is opened + directly from a project folder. + """ + inPath = Path(path) + if inPath.is_file(): + inPath = inPath.parent + + if not inPath.is_dir(): + logger.error("No such folder: %s", inPath) + self.clear() + return False + + self._storagePath = inPath + self._runtimePath = inPath + self._openMode = self.MODE_INPLACE + + return True def openProjectArchive(self, path): pass - def close(self): - pass + def runPostSaveTasks(self, autoSave=False): + """Run tasks after the project has been saved. + """ + if self._openMode == self.MODE_INPLACE: + # Nothing to do, so we just return + return True + + return True + + def closeSession(self): + """Run tasks related to closing the session. + """ + # Clear lockfile + self.clear() + return ## # Content Access Methods ## def getXmlReader(self): - pass + """ + """ + if self._runtimePath is None: + return None + + projFile = self._runtimePath / nwFiles.PROJ_FILE + xmlReader = ProjectXMLReader(projFile) + + return xmlReader def getXmlWriter(self): - pass + """ + """ + if self._runtimePath is None: + return None + + xmlWriter = ProjectXMLWriter(self._runtimePath) + + return xmlWriter def getDocument(self, tHandle): pass diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index 05580607..59943220 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -220,12 +220,12 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir # Fail saving with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) + mp.setattr("pathlib.Path.write_bytes", causeOSError) assert xmlWriter.write(data, packedContent, timeStamp, 1000) is False assert str(xmlWriter.error) == "Mock OSError" with monkeypatch.context() as mp: - mp.setattr("os.replace", causeOSError) + mp.setattr("pathlib.Path.replace", causeOSError) assert xmlWriter.write(data, packedContent, timeStamp, 1000) is False assert str(xmlWriter.error) == "Mock OSError" diff --git a/tests/tools.py b/tests/tools.py index 0da846cc..9a2e4dd8 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -167,6 +167,7 @@ def buildTestProject(theObject, projPath): theProject.clearProject() theProject.setProjectPath(projPath, newProject=True) + theProject.storage.openProjectInPlace(theProject.projPath) theProject.data.itemStatus.write(None, "New", (100, 100, 100)) theProject.data.itemStatus.write(None, "Note", (200, 50, 0))