Rewrite project consistency chech as part of the tree class

This commit is contained in:
Veronica Berglyd Olsen
2023-07-21 23:56:48 +02:00
parent f0e372b2ba
commit 3753f5b829
5 changed files with 86 additions and 129 deletions
+8 -2
View File
@@ -1,7 +1,6 @@
"""
novelWriter Project Document
==============================
Data class for a single novelWriter document
File History:
Created: 2018-09-29 [0.0.1]
@@ -41,8 +40,15 @@ logger = logging.getLogger(__name__)
class NWDocument:
"""Core: Document Class
def __init__(self, project: NWProject, tHandle: str) -> None:
A Class wrapping a single novelWriter document file. It represents
a project item of nwItemType FILE. The file is not guaranteed to
exist, even if the item does. In the case it doesn't exist, reading
it returns a None rather than an empty or non-empty string.
"""
def __init__(self, project: NWProject, tHandle: str | None) -> None:
self._project = project
+2 -2
View File
@@ -76,7 +76,7 @@ class NWIndex:
a rebuild of the index data.
"""
def __init__(self, project):
def __init__(self, project: NWProject):
self._project = project
@@ -197,7 +197,7 @@ class NWIndex:
logger.debug("Checking index")
# Check that all files are indexed
for fHandle in self._project.projFiles:
for fHandle in self._project.storage.scanContent():
if fHandle not in self._itemIndex:
logger.warning("Item '%s' is not in the index", fHandle)
self.reIndexHandle(fHandle)
+9 -116
View File
@@ -45,7 +45,7 @@ from novelwriter.core.sessions import NWSessionLog
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.core.projectdata import NWProjectData
from novelwriter.common import (
checkStringNone, formatTimeStamp, hexToInt, isHandle, makeFileNameSafe, minmax
checkStringNone, formatTimeStamp, hexToInt, makeFileNameSafe, minmax
)
logger = logging.getLogger(__name__)
@@ -75,7 +75,6 @@ class NWProject(QObject):
# Project Status
self._projChanged = False # The project has unsaved changes
self._lockedBy = None # Data on which computer has the project open
self._projFiles = [] # A list of all files in the content folder on load
# Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject")
@@ -121,10 +120,6 @@ class NWProject(QObject):
def projChanged(self):
return self._projChanged
@property
def projFiles(self):
return self._projFiles
##
# Item Methods
##
@@ -213,9 +208,6 @@ class NWProject(QObject):
self._data = NWProjectData(self)
self._session = NWSessionLog(self)
# Project Settings
self._projFiles = []
return
def openProject(self, projPath, overrideLock=False):
@@ -330,15 +322,13 @@ class NWProject(QObject):
)
# Check the project tree consistency
for tItem in self._tree:
if tItem:
tHandle = tItem.itemHandle
logger.debug("Checking item '%s'", tHandle)
if not self._tree.updateItemData(tHandle):
logger.error("There was a problem the item, and it has been removed")
del self._tree[tHandle] # The file will be re-added as orphaned
# This also handles any orphaned files found
orphans, recovered = self._tree.checkConsistency(self.tr("Recovered"))
if orphans > 0:
self.mainGui.makeAlert(self.tr(
"Found {0} orphaned file(s) in the project. {1} file(s) were recovered."
).format(orphans, recovered), nwAlert.WARN)
self._scanProjectFolder()
self._index.loadIndex()
if xmlReader.state == XMLReadState.WAS_LEGACY:
# Often, the index needs to be rebuilt when updating format
@@ -646,9 +636,8 @@ class NWProject(QObject):
return True
def _loadProjectLocalisation(self):
"""Load the language data for the current project language.
"""
def _loadProjectLocalisation(self) -> bool:
"""Load the language data for the current project language."""
if self._data.language is None or CONFIG._nwLangPath is None:
self._langData = {}
return False
@@ -661,7 +650,6 @@ class NWProject(QObject):
with open(langFile, mode="r", encoding="utf-8") as inFile:
self._langData = json.load(inFile)
logger.debug("Loaded project language file: %s", langFile.name)
except Exception:
logger.error("Failed to project language file")
logException()
@@ -669,99 +657,4 @@ class NWProject(QObject):
return True
def _scanProjectFolder(self):
"""Scan the project folder and check that the files in it are
also in the project XML file. If they aren't, import them as
orphaned files so the user can either delete them, or put them
back into the project tree.
"""
contentPath = self._storage.contentPath
if not isinstance(contentPath, Path):
return False
# Then check the files in the data folder
logger.debug("Checking files in project content folder")
orphanFiles = []
self._projFiles = []
for item in contentPath.iterdir():
itemName = item.name
if not itemName.endswith(".nwd"):
logger.warning("Skipping file: %s", itemName)
continue
if len(itemName) != 17:
logger.warning("Skipping file: %s", itemName)
continue
fHandle = itemName[:13]
if not isHandle(fHandle):
logger.warning("Skipping file: %s", itemName)
continue
if fHandle in self._tree:
self._projFiles.append(fHandle)
logger.debug("Checking file %s, handle '%s': OK", itemName, fHandle)
else:
logger.warning("Checking file %s, handle '%s': Orphaned", itemName, fHandle)
orphanFiles.append(fHandle)
# Report status
if len(orphanFiles) > 0:
self.mainGui.makeAlert(self.tr(
"Found {0} orphaned file(s) in project folder."
).format(len(orphanFiles)), nwAlert.WARN)
else:
logger.debug("File check OK")
return
# Handle orphans
nOrph = 0
noWhere = False
oPrefix = self.tr("Recovered")
for oHandle in orphanFiles:
# Look for meta data
oName = ""
oParent = None
oClass = None
oLayout = None
aDoc = self._storage.getDocument(oHandle)
if aDoc.readDocument(isOrphan=True) is not None:
oName, oParent, oClass, oLayout = aDoc.getMeta()
if oName:
oName = self.tr("[{0}] {1}").format(
oPrefix, oName.replace("[%s]" % oPrefix, "").strip()
)
else:
nOrph += 1
oName = self.tr("Recovered File {0}").format(nOrph)
# Recover file meta data
oClass = oClass or nwItemClass.NOVEL
oLayout = oLayout or nwItemLayout.NOTE
if oParent is None or oParent not in self._tree:
oParent = self._tree.findRoot(oClass)
if oParent is None:
oParent = self._tree.findRoot(nwItemClass.NOVEL)
# If the file still has no parent item, skip it
if oParent is None:
noWhere = True
continue
nHandle = self._tree.create(oName, oParent, nwItemType.FILE, oClass, oLayout)
if nHandle is not None:
(contentPath / f"{oHandle}.nwd").rename(contentPath / f"{nHandle}.nwd")
if noWhere:
self.mainGui.makeAlert(self.tr(
"One or more orphaned files could not be added back into the project. "
"Make sure at least a Novel root folder exists."
), nwAlert.WARN)
return True
# END Class NWProject
+11 -1
View File
@@ -33,7 +33,7 @@ from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
from novelwriter import CONFIG
from novelwriter.error import logException
from novelwriter.common import minmax
from novelwriter.common import isHandle, minmax
from novelwriter.constants import nwFiles
from novelwriter.core.document import NWDocument
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
@@ -179,6 +179,16 @@ class NWStorage:
return self._runtimePath / "meta" / fileName
return None
def scanContent(self) -> list[str]:
"""Scan the content folder and return the handle of all files
found in it. Files that do not match the pattern are ignored.
"""
contentPath = self.contentPath
return [
item.stem for item in contentPath.iterdir()
if item.suffix == ".nwd" and isHandle(item.stem)
] if contentPath else []
def readLockFile(self) -> list:
"""Read the project lock file."""
if self._lockFilePath is None:
+56 -8
View File
@@ -95,18 +95,15 @@ class NWTree:
@overload
def create(self, label: str, parent: None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS,
itemLayout: nwItemLayout = nwItemLayout.NO_LAYOUT) -> str:
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str:
...
@overload
def create(self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS,
itemLayout: nwItemLayout = nwItemLayout.NO_LAYOUT) -> str | None:
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None:
...
def create(self, label, parent, itemType,
itemClass=nwItemClass.NO_CLASS, itemLayout=nwItemLayout.NO_LAYOUT):
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS):
"""Create a new item in the project tree, and return its handle.
If the item cannot be added to the project, None is returned.
"""
@@ -117,7 +114,6 @@ class NWTree:
newItem.setParent(parent)
newItem.setType(itemType)
newItem.setClass(itemClass)
newItem.setLayout(itemLayout)
self.append(newItem)
self.updateItemData(tHandle)
return tHandle
@@ -185,12 +181,64 @@ class NWTree:
"""
self.clear()
for item in data:
nwItem = NWItem(self._project, "NOTSET") # Handle is set by unpack()
nwItem = NWItem(self._project, "") # Handle is set by unpack()
if nwItem.unpack(item):
self.append(nwItem)
nwItem.saveInitialCount()
return
def checkConsistency(self, prefix: str) -> tuple[int, int]:
"""Check the project tree consistency. Also check the content
folder and add back files that were discovered but were not
included in the tree. This function should only be called after
the project file has been processed, but before the loading of
the project returns. The functions requires a prefix string to
mark recovered files.
"""
for tHandle in self._treeOrder:
if self.updateItemData(tHandle):
logger.debug("Checking item '%s' ... OK", tHandle)
else:
logger.error("Checking item '%s' ... ERROR", tHandle)
self.__delitem__(tHandle) # The file will be re-added as orphaned
orphans = 0
recovered = 0
storage = self._project.storage
for cHandle in storage.scanContent():
if cHandle in self._treeOrder:
continue
orphans += 1
aDoc = storage.getDocument(cHandle)
aDoc.readDocument(isOrphan=True)
oName, oParent, oClass, oLayout = aDoc.getMeta()
oName = oName or cHandle
oParent = oParent if oParent in self._treeOrder else None
oClass = oClass or nwItemClass.NOVEL
oLayout = oLayout or nwItemLayout.NOTE
# If the parent doesn't exists, find a new home
if oParent is None: # Add it to the first available class root
oParent = self.findRoot(oClass)
if oParent is None: # Otherwise, add to the Novel root
oParent = self.findRoot(nwItemClass.NOVEL)
if oParent is None: # If not, give up
continue
# Create a new item
newItem = NWItem(self._project, cHandle)
newItem.setName(f"[{prefix}] {oName}")
newItem.setParent(oParent)
newItem.setType(nwItemType.FILE)
newItem.setClass(oClass)
newItem.setLayout(oLayout)
if self.append(newItem):
recovered += 1
return orphans, recovered
def writeToCFile(self) -> bool:
"""Write the convenience table of contents file in the root of
the project directory.