Use storage class to open and save project XML
This commit is contained in:
@@ -105,20 +105,24 @@ class NWProject(QObject):
|
|||||||
##
|
##
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def options(self):
|
||||||
return self._data
|
return self._options
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def index(self):
|
def storage(self):
|
||||||
return self._index
|
return self._storage
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tree(self):
|
def tree(self):
|
||||||
return self._tree
|
return self._tree
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def options(self):
|
def index(self):
|
||||||
return self._options
|
return self._index
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def projOpened(self):
|
def projOpened(self):
|
||||||
@@ -245,6 +249,7 @@ class NWProject(QObject):
|
|||||||
self._projAltered = False
|
self._projAltered = False
|
||||||
|
|
||||||
# Project Tree
|
# Project Tree
|
||||||
|
self._storage.clear()
|
||||||
self._tree.clear()
|
self._tree.clear()
|
||||||
self._index.clearIndex()
|
self._index.clearIndex()
|
||||||
self._data = NWProjectData(self)
|
self._data = NWProjectData(self)
|
||||||
@@ -302,6 +307,8 @@ class NWProject(QObject):
|
|||||||
if not self.setProjectPath(projPath, newProject=True):
|
if not self.setProjectPath(projPath, newProject=True):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
self._storage.openProjectInPlace(self.projPath)
|
||||||
|
|
||||||
self._data.setName(projName)
|
self._data.setName(projName)
|
||||||
self._data.setTitle(projTitle)
|
self._data.setTitle(projTitle)
|
||||||
self._data.setAuthors(projAuthors)
|
self._data.setAuthors(projAuthors)
|
||||||
@@ -458,10 +465,18 @@ class NWProject(QObject):
|
|||||||
# Open The Project XML File
|
# 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)
|
self._data = NWProjectData(self)
|
||||||
projContent = []
|
projContent = []
|
||||||
|
|
||||||
xmlReader = ProjectXMLReader(fileName)
|
|
||||||
xmlParsed = xmlReader.read(self._data, projContent)
|
xmlParsed = xmlReader.read(self._data, projContent)
|
||||||
|
|
||||||
appVersion = xmlReader.appVersion or self.tr("Unknown")
|
appVersion = xmlReader.appVersion or self.tr("Unknown")
|
||||||
@@ -580,6 +595,12 @@ class NWProject(QObject):
|
|||||||
), nwAlert.ERROR)
|
), nwAlert.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not self._storage.isOpen():
|
||||||
|
self.mainGui.makeAlert(self.tr(
|
||||||
|
"There is no project open."
|
||||||
|
), nwAlert.ERROR)
|
||||||
|
return False
|
||||||
|
|
||||||
saveTime = time()
|
saveTime = time()
|
||||||
if not self.ensureFolderStructure():
|
if not self.ensureFolderStructure():
|
||||||
return False
|
return False
|
||||||
@@ -594,11 +615,13 @@ class NWProject(QObject):
|
|||||||
self.updateWordCounts()
|
self.updateWordCounts()
|
||||||
self.countStatus()
|
self.countStatus()
|
||||||
|
|
||||||
|
xmlWriter = self._storage.getXmlWriter()
|
||||||
|
if not isinstance(xmlWriter, ProjectXMLWriter):
|
||||||
|
return False
|
||||||
|
|
||||||
saveTime = time()
|
saveTime = time()
|
||||||
editTime = int(self._data.editTime + saveTime - self._projOpened)
|
editTime = int(self._data.editTime + saveTime - self._projOpened)
|
||||||
|
|
||||||
content = self._tree.pack()
|
content = self._tree.pack()
|
||||||
xmlWriter = ProjectXMLWriter(self.projPath)
|
|
||||||
if not xmlWriter.write(self._data, content, saveTime, editTime):
|
if not xmlWriter.write(self._data, content, saveTime, editTime):
|
||||||
self.mainGui.makeAlert(self.tr(
|
self.mainGui.makeAlert(self.tr(
|
||||||
"Failed to save project."
|
"Failed to save project."
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import novelwriter
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from time import time
|
from time import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString
|
checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString
|
||||||
@@ -91,7 +92,7 @@ class ProjectXMLReader:
|
|||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
|
|
||||||
self._path = path
|
self._path = Path(path)
|
||||||
self._state = XMLReadState.NO_ACTION
|
self._state = XMLReadState.NO_ACTION
|
||||||
|
|
||||||
self._root = ""
|
self._root = ""
|
||||||
@@ -153,7 +154,7 @@ class ProjectXMLReader:
|
|||||||
logger.debug("Reading project XML")
|
logger.debug("Reading project XML")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xml = etree.parse(self._path)
|
xml = etree.parse(str(self._path))
|
||||||
self._state = XMLReadState.NO_ERROR
|
self._state = XMLReadState.NO_ERROR
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -161,10 +162,10 @@ class ProjectXMLReader:
|
|||||||
logger.error("Failed to parse project XML", exc_info=exc)
|
logger.error("Failed to parse project XML", exc_info=exc)
|
||||||
self._state = XMLReadState.CANNOT_PARSE
|
self._state = XMLReadState.CANNOT_PARSE
|
||||||
|
|
||||||
backFile = self._path[:-3]+"bak"
|
backFile = self._path.with_suffix(".bak")
|
||||||
if os.path.isfile(backFile):
|
if os.path.isfile(backFile):
|
||||||
try:
|
try:
|
||||||
xml = etree.parse(backFile)
|
xml = etree.parse(str(backFile))
|
||||||
self._state = XMLReadState.PARSED_BACKUP
|
self._state = XMLReadState.PARSED_BACKUP
|
||||||
logger.info("Backup project file parsed")
|
logger.info("Backup project file parsed")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -445,7 +446,7 @@ class ProjectXMLWriter:
|
|||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
|
|
||||||
self._path = path
|
self._path = Path(path)
|
||||||
self._error = None
|
self._error = None
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -514,17 +515,13 @@ class ProjectXMLWriter:
|
|||||||
xName.text = item["name"]
|
xName.text = item["name"]
|
||||||
|
|
||||||
# Write the XML tree to file
|
# Write the XML tree to file
|
||||||
saveFile = os.path.join(self._path, nwFiles.PROJ_FILE)
|
saveFile = self._path / nwFiles.PROJ_FILE
|
||||||
tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~")
|
tempFile = saveFile.with_suffix(".tmp")
|
||||||
backFile = os.path.join(self._path, nwFiles.PROJ_FILE[:-3]+"bak")
|
backFile = saveFile.with_suffix(".bak")
|
||||||
try:
|
try:
|
||||||
with open(tempFile, mode="wb") as outFile:
|
tempFile.write_bytes(etree.tostring(
|
||||||
outFile.write(etree.tostring(
|
xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True
|
||||||
xRoot,
|
))
|
||||||
pretty_print=True,
|
|
||||||
encoding="utf-8",
|
|
||||||
xml_declaration=True
|
|
||||||
))
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._error = exc
|
self._error = exc
|
||||||
return False
|
return False
|
||||||
@@ -532,10 +529,10 @@ class ProjectXMLWriter:
|
|||||||
# If we're here, the file was successfully saved,
|
# If we're here, the file was successfully saved,
|
||||||
# so let's sort out the temps and backups
|
# so let's sort out the temps and backups
|
||||||
try:
|
try:
|
||||||
if os.path.isfile(saveFile):
|
if saveFile.exists():
|
||||||
os.replace(saveFile, backFile)
|
saveFile.replace(backFile)
|
||||||
os.replace(tempFile, saveFile)
|
tempFile.replace(saveFile)
|
||||||
except OSError as exc:
|
except Exception as exc:
|
||||||
self._error = exc
|
self._error = exc
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -25,37 +25,109 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from novelwriter.constants import nwFiles
|
||||||
|
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NWStorage:
|
class NWStorage:
|
||||||
|
|
||||||
|
MODE_INACTIVE = 0
|
||||||
|
MODE_INPLACE = 1
|
||||||
|
MODE_ARCHIVE = 2
|
||||||
|
|
||||||
def __init__(self, theProject):
|
def __init__(self, theProject):
|
||||||
|
|
||||||
self.theProject = 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
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Core Methods
|
# Core Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def openProjectFolder(self, path):
|
def isOpen(self):
|
||||||
pass
|
"""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):
|
def openProjectArchive(self, path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def close(self):
|
def runPostSaveTasks(self, autoSave=False):
|
||||||
pass
|
"""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
|
# Content Access Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def getXmlReader(self):
|
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):
|
def getXmlWriter(self):
|
||||||
pass
|
"""
|
||||||
|
"""
|
||||||
|
if self._runtimePath is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
xmlWriter = ProjectXMLWriter(self._runtimePath)
|
||||||
|
|
||||||
|
return xmlWriter
|
||||||
|
|
||||||
def getDocument(self, tHandle):
|
def getDocument(self, tHandle):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -220,12 +220,12 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
|
|||||||
|
|
||||||
# Fail saving
|
# Fail saving
|
||||||
with monkeypatch.context() as mp:
|
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 xmlWriter.write(data, packedContent, timeStamp, 1000) is False
|
||||||
assert str(xmlWriter.error) == "Mock OSError"
|
assert str(xmlWriter.error) == "Mock OSError"
|
||||||
|
|
||||||
with monkeypatch.context() as mp:
|
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 xmlWriter.write(data, packedContent, timeStamp, 1000) is False
|
||||||
assert str(xmlWriter.error) == "Mock OSError"
|
assert str(xmlWriter.error) == "Mock OSError"
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ def buildTestProject(theObject, projPath):
|
|||||||
|
|
||||||
theProject.clearProject()
|
theProject.clearProject()
|
||||||
theProject.setProjectPath(projPath, newProject=True)
|
theProject.setProjectPath(projPath, newProject=True)
|
||||||
|
theProject.storage.openProjectInPlace(theProject.projPath)
|
||||||
|
|
||||||
theProject.data.itemStatus.write(None, "New", (100, 100, 100))
|
theProject.data.itemStatus.write(None, "New", (100, 100, 100))
|
||||||
theProject.data.itemStatus.write(None, "Note", (200, 50, 0))
|
theProject.data.itemStatus.write(None, "Note", (200, 50, 0))
|
||||||
|
|||||||
Reference in New Issue
Block a user