Use storage class to open and save project XML
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -25,37 +25,109 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user