Rewrite project open and creation in storage class
This commit is contained in:
@@ -340,7 +340,7 @@ class ProjectBuilder:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
if not project.storage.openProjectInPlace(projPath, newProject=True):
|
if not project.storage.createNewProject(projPath):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
lblNewProject = self.tr("New Project")
|
lblNewProject = self.tr("New Project")
|
||||||
|
|||||||
+38
-34
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
from time import time
|
from time import time
|
||||||
from typing import TYPE_CHECKING, Iterator
|
from typing import TYPE_CHECKING, Iterator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -40,7 +41,7 @@ from novelwriter.constants import trConst, nwLabels
|
|||||||
from novelwriter.core.tree import NWTree
|
from novelwriter.core.tree import NWTree
|
||||||
from novelwriter.core.index import NWIndex
|
from novelwriter.core.index import NWIndex
|
||||||
from novelwriter.core.options import OptionState
|
from novelwriter.core.options import OptionState
|
||||||
from novelwriter.core.storage import NWStorage
|
from novelwriter.core.storage import NWStorage, NWStorageOpen
|
||||||
from novelwriter.core.sessions import NWSessionLog
|
from novelwriter.core.sessions import NWSessionLog
|
||||||
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
||||||
from novelwriter.core.projectdata import NWProjectData
|
from novelwriter.core.projectdata import NWProjectData
|
||||||
@@ -55,6 +56,16 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NWProjectState(Enum):
|
||||||
|
|
||||||
|
UNKNOWN = 0
|
||||||
|
LOCKED = 1
|
||||||
|
RECOVERY = 2
|
||||||
|
READY = 3
|
||||||
|
|
||||||
|
# END Enum NWProjectState
|
||||||
|
|
||||||
|
|
||||||
class NWProject:
|
class NWProject:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -69,9 +80,9 @@ class NWProject:
|
|||||||
|
|
||||||
# Project Status
|
# Project Status
|
||||||
self._langData = {} # Localisation data
|
self._langData = {} # Localisation data
|
||||||
self._lockedBy = None # Data on which computer has the project open
|
|
||||||
self._changed = False # The project has unsaved changes
|
self._changed = False # The project has unsaved changes
|
||||||
self._valid = False # The project was successfully loaded
|
self._valid = False # The project was successfully loaded
|
||||||
|
self._state = NWProjectState.UNKNOWN
|
||||||
|
|
||||||
# Internal Mapping
|
# Internal Mapping
|
||||||
self.tr = partial(QCoreApplication.translate, "NWProject")
|
self.tr = partial(QCoreApplication.translate, "NWProject")
|
||||||
@@ -125,12 +136,15 @@ class NWProject:
|
|||||||
"""Return True if a project is loaded."""
|
"""Return True if a project is loaded."""
|
||||||
return self._valid
|
return self._valid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> NWProjectState:
|
||||||
|
"""Return the current project state."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lockStatus(self) -> list | None:
|
def lockStatus(self) -> list | None:
|
||||||
"""Return the project lock information."""
|
"""Return the project lock information."""
|
||||||
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
|
return self._storage.lockStatus
|
||||||
return self._lockedBy
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def currentEditTime(self) -> int:
|
def currentEditTime(self) -> int:
|
||||||
@@ -219,29 +233,23 @@ class NWProject:
|
|||||||
build the tree of project items.
|
build the tree of project items.
|
||||||
"""
|
"""
|
||||||
logger.info("Opening project: %s", projPath)
|
logger.info("Opening project: %s", projPath)
|
||||||
if not self._storage.openProjectInPlace(projPath):
|
|
||||||
SHARED.error(self.tr("Could not open project with path: {0}").format(projPath))
|
status = self._storage.initProjectStorage(projPath, clearLock)
|
||||||
|
if status != NWStorageOpen.READY:
|
||||||
|
if status == NWStorageOpen.UNKOWN:
|
||||||
|
SHARED.error(self.tr("Not a known project file format."))
|
||||||
|
elif status == NWStorageOpen.NOT_FOUND:
|
||||||
|
SHARED.error(self.tr("Project file not found."))
|
||||||
|
elif status == NWStorageOpen.LOCKED:
|
||||||
|
self._state = NWProjectState.LOCKED
|
||||||
|
elif status == NWStorageOpen.RECOVERY:
|
||||||
|
self._state = NWProjectState.RECOVERY
|
||||||
|
elif status == NWStorageOpen.FAILED:
|
||||||
|
SHARED.error(self.tr("Failed to open project."), exc=self._storage.exc)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Project Lock
|
# Read Project XML
|
||||||
# ============
|
# ================
|
||||||
|
|
||||||
if clearLock:
|
|
||||||
self._storage.clearLockFile()
|
|
||||||
|
|
||||||
lockStatus = self._storage.readLockFile()
|
|
||||||
if len(lockStatus) > 0:
|
|
||||||
if lockStatus[0] == "ERROR":
|
|
||||||
logger.warning("Failed to check lock file")
|
|
||||||
else:
|
|
||||||
logger.error("Project is locked, so not opening")
|
|
||||||
self._lockedBy = lockStatus
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.debug("Project is not locked")
|
|
||||||
|
|
||||||
# Open The Project XML File
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
xmlReader = self._storage.getXmlReader()
|
xmlReader = self._storage.getXmlReader()
|
||||||
if not isinstance(xmlReader, ProjectXMLReader):
|
if not isinstance(xmlReader, ProjectXMLReader):
|
||||||
@@ -250,9 +258,7 @@ class NWProject:
|
|||||||
self._data = NWProjectData(self)
|
self._data = NWProjectData(self)
|
||||||
projContent = []
|
projContent = []
|
||||||
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")
|
||||||
|
|
||||||
if not xmlParsed:
|
if not xmlParsed:
|
||||||
if xmlReader.state == XMLReadState.NOT_NWX_FILE:
|
if xmlReader.state == XMLReadState.NOT_NWX_FILE:
|
||||||
SHARED.error(self.tr(
|
SHARED.error(self.tr(
|
||||||
@@ -323,9 +329,9 @@ class NWProject:
|
|||||||
|
|
||||||
self.updateWordCounts()
|
self.updateWordCounts()
|
||||||
self._session.startSession()
|
self._session.startSession()
|
||||||
self._storage.writeLockFile()
|
|
||||||
self.setProjectChanged(False)
|
self.setProjectChanged(False)
|
||||||
self._valid = True
|
self._valid = True
|
||||||
|
self._state = NWProjectState.READY
|
||||||
|
|
||||||
SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
|
SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
|
||||||
|
|
||||||
@@ -370,13 +376,11 @@ class NWProject:
|
|||||||
self._storage.runPostSaveTasks(autoSave=autoSave)
|
self._storage.runPostSaveTasks(autoSave=autoSave)
|
||||||
|
|
||||||
# Update recent projects
|
# Update recent projects
|
||||||
storePath = self._storage.storagePath
|
if storagePath := self._storage.storagePath:
|
||||||
if storePath:
|
|
||||||
CONFIG.recentProjects.update(
|
CONFIG.recentProjects.update(
|
||||||
storePath, self._data.name, sum(self._data.currCounts), saveTime
|
storagePath, self._data.name, sum(self._data.currCounts), saveTime
|
||||||
)
|
)
|
||||||
|
|
||||||
self._storage.writeLockFile()
|
|
||||||
SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
|
SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
|
||||||
self.setProjectChanged(False)
|
self.setProjectChanged(False)
|
||||||
|
|
||||||
@@ -420,8 +424,8 @@ class NWProject:
|
|||||||
timeStamp = formatTimeStamp(time(), fileSafe=True)
|
timeStamp = formatTimeStamp(time(), fileSafe=True)
|
||||||
archName = baseDir / f"{cleanName} {timeStamp}.zip"
|
archName = baseDir / f"{cleanName} {timeStamp}.zip"
|
||||||
if self._storage.zipIt(archName, compression=2):
|
if self._storage.zipIt(archName, compression=2):
|
||||||
size = formatInt(getFileSize(archName))
|
|
||||||
if doNotify:
|
if doNotify:
|
||||||
|
size = formatInt(getFileSize(archName))
|
||||||
SHARED.info(
|
SHARED.info(
|
||||||
self.tr("Created a backup of your project of size {0}B.").format(size),
|
self.tr("Created a backup of your project of size {0}B.").format(size),
|
||||||
info=self.tr("Path: {0}").format(str(backupPath))
|
info=self.tr("Path: {0}").format(str(backupPath))
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ from novelwriter.common import (
|
|||||||
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
|
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
|
||||||
hexToInt, simplified, xmlIndent, yesNo
|
hexToInt, simplified, xmlIndent, yesNo
|
||||||
)
|
)
|
||||||
from novelwriter.constants import nwFiles
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from novelwriter.core.status import NWStatus
|
from novelwriter.core.status import NWStatus
|
||||||
@@ -558,9 +557,8 @@ class ProjectXMLWriter:
|
|||||||
xName.text = item["name"]
|
xName.text = item["name"]
|
||||||
|
|
||||||
# Write the XML tree to file
|
# Write the XML tree to file
|
||||||
saveFile = self._path / nwFiles.PROJ_FILE
|
tempFile = self._path.with_suffix(".tmp")
|
||||||
tempFile = saveFile.with_suffix(".tmp")
|
backFile = self._path.with_suffix(".bak")
|
||||||
backFile = saveFile.with_suffix(".bak")
|
|
||||||
try:
|
try:
|
||||||
xml = ET.ElementTree(xRoot)
|
xml = ET.ElementTree(xRoot)
|
||||||
xmlIndent(xml)
|
xmlIndent(xml)
|
||||||
@@ -572,9 +570,9 @@ 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 saveFile.exists():
|
if self._path.exists():
|
||||||
saveFile.replace(backFile)
|
self._path.replace(backFile)
|
||||||
tempFile.replace(saveFile)
|
tempFile.replace(self._path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._error = exc
|
self._error = exc
|
||||||
return False
|
return False
|
||||||
|
|||||||
+169
-136
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
from time import time
|
from time import time
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -45,6 +46,18 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NWStorageOpen(Enum):
|
||||||
|
|
||||||
|
UNKOWN = 0
|
||||||
|
NOT_FOUND = 1
|
||||||
|
LOCKED = 2
|
||||||
|
RECOVERY = 3
|
||||||
|
FAILED = 4
|
||||||
|
READY = 5
|
||||||
|
|
||||||
|
# END Enum NWStorageOpen
|
||||||
|
|
||||||
|
|
||||||
class NWStorage:
|
class NWStorage:
|
||||||
"""Core: Project Storage Class
|
"""Core: Project Storage Class
|
||||||
|
|
||||||
@@ -60,7 +73,10 @@ class NWStorage:
|
|||||||
self._storagePath = None
|
self._storagePath = None
|
||||||
self._runtimePath = None
|
self._runtimePath = None
|
||||||
self._lockFilePath = None
|
self._lockFilePath = None
|
||||||
|
self._lockedBy = None
|
||||||
self._openMode = self.MODE_INACTIVE
|
self._openMode = self.MODE_INACTIVE
|
||||||
|
self._ready = False
|
||||||
|
self._exception = None
|
||||||
return
|
return
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
@@ -69,6 +85,8 @@ class NWStorage:
|
|||||||
self._runtimePath = None
|
self._runtimePath = None
|
||||||
self._lockFilePath = None
|
self._lockFilePath = None
|
||||||
self._openMode = self.MODE_INACTIVE
|
self._openMode = self.MODE_INACTIVE
|
||||||
|
self._ready = False
|
||||||
|
self._exception = None
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -77,12 +95,12 @@ class NWStorage:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def storagePath(self) -> Path | None:
|
def storagePath(self) -> Path | None:
|
||||||
"""Get the path where the project is saved."""
|
"""Return the path where the project is stored."""
|
||||||
return self._storagePath
|
return self._storagePath
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def runtimePath(self) -> Path | None:
|
def runtimePath(self) -> Path | None:
|
||||||
"""Get the path where the project is saved at runtime."""
|
"""Return the path where the project is stored at runtime."""
|
||||||
return self._runtimePath
|
return self._runtimePath
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -97,6 +115,18 @@ class NWStorage:
|
|||||||
logger.error("Content path cannot be resolved")
|
logger.error("Content path cannot be resolved")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lockStatus(self) -> list | None:
|
||||||
|
"""Return the project lock information."""
|
||||||
|
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
|
||||||
|
return self._lockedBy
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exc(self) -> Exception | None:
|
||||||
|
"""Return the latest exception of the storage instance."""
|
||||||
|
return self._exception
|
||||||
|
|
||||||
##
|
##
|
||||||
# Core Methods
|
# Core Methods
|
||||||
##
|
##
|
||||||
@@ -105,18 +135,11 @@ class NWStorage:
|
|||||||
"""Check if the storage location is open."""
|
"""Check if the storage location is open."""
|
||||||
return self._runtimePath is not None
|
return self._runtimePath is not None
|
||||||
|
|
||||||
def openProjectInPlace(self, path: str | Path, newProject: bool = False) -> bool:
|
def createNewProject(self, path: str | Path) -> bool:
|
||||||
"""Open a novelWriter project in-place. That is, it is opened
|
"""Create a new project at the given location."""
|
||||||
directly from a project folder.
|
|
||||||
"""
|
|
||||||
inPath = Path(path).resolve()
|
inPath = Path(path).resolve()
|
||||||
if inPath.is_file():
|
if inPath.is_dir() and len(list(inPath.iterdir())) > 0:
|
||||||
# The path should not point to an existing file,
|
logger.error("Folder is not empty: %s", inPath)
|
||||||
# but it can point to a folder containing files
|
|
||||||
inPath = inPath.parent
|
|
||||||
|
|
||||||
if not (inPath.is_dir() or newProject):
|
|
||||||
# If the project is not new, the folder must already exist.
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._storagePath = inPath
|
self._storagePath = inPath
|
||||||
@@ -124,17 +147,93 @@ class NWStorage:
|
|||||||
self._lockFilePath = inPath / nwFiles.PROJ_LOCK
|
self._lockFilePath = inPath / nwFiles.PROJ_LOCK
|
||||||
self._openMode = self.MODE_INPLACE
|
self._openMode = self.MODE_INPLACE
|
||||||
|
|
||||||
if not self._prepareStorage(checkLegacy=True, newProject=newProject):
|
basePath = self._runtimePath
|
||||||
|
metaPath = basePath / "meta"
|
||||||
|
contPath = basePath / "content"
|
||||||
|
try:
|
||||||
|
basePath.mkdir(exist_ok=True)
|
||||||
|
metaPath.mkdir(exist_ok=True)
|
||||||
|
contPath.mkdir(exist_ok=True)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Failed to create project folders", exc_info=exc)
|
||||||
self.clear()
|
self.clear()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
self._ready = True
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def openProjectArchive(self, path: str | Path) -> bool: # pragma: no cover
|
def initProjectStorage(self, path: str | Path, clearLock: bool = False) -> NWStorageOpen:
|
||||||
"""Open the project from a single file.
|
"""Initialise the a novelWriter project."""
|
||||||
Placeholder for later implementation. See #977.
|
inPath = Path(path).resolve()
|
||||||
"""
|
|
||||||
return False
|
# Initialise Storage Instance
|
||||||
|
# ===========================
|
||||||
|
|
||||||
|
# Check what we're opening. Only three options are allowed:
|
||||||
|
# 1. A folder with a nwProject.nwx file in it (not home)
|
||||||
|
# 2. A full path to an nwProject.nwx file
|
||||||
|
if inPath.is_dir() and inPath != Path.home().resolve():
|
||||||
|
nwxFile = inPath / nwFiles.PROJ_FILE
|
||||||
|
elif inPath.is_file() and inPath.name == nwFiles.PROJ_FILE:
|
||||||
|
nwxFile = inPath
|
||||||
|
else:
|
||||||
|
logger.error("Not a novelWriter project")
|
||||||
|
return NWStorageOpen.UNKOWN
|
||||||
|
|
||||||
|
if not nwxFile.exists():
|
||||||
|
# The .nwx file must exist to continue
|
||||||
|
logger.error("Not found: %s", nwxFile)
|
||||||
|
return NWStorageOpen.NOT_FOUND
|
||||||
|
|
||||||
|
nwxPath = nwxFile.parent
|
||||||
|
|
||||||
|
self._storagePath = nwxPath
|
||||||
|
self._runtimePath = nwxPath
|
||||||
|
self._lockFilePath = nwxPath / nwFiles.PROJ_LOCK
|
||||||
|
self._openMode = self.MODE_INPLACE
|
||||||
|
|
||||||
|
# Check Project Lock
|
||||||
|
# ==================
|
||||||
|
|
||||||
|
if clearLock:
|
||||||
|
self._clearLockFile()
|
||||||
|
|
||||||
|
self._readLockFile()
|
||||||
|
if self._lockedBy and len(self._lockedBy) == 4:
|
||||||
|
if self._lockedBy[0] == "ERROR":
|
||||||
|
logger.warning("Failed to check lock file")
|
||||||
|
else:
|
||||||
|
logger.error("Project is locked, so not opening")
|
||||||
|
return NWStorageOpen.LOCKED
|
||||||
|
else:
|
||||||
|
logger.debug("Project is not locked")
|
||||||
|
|
||||||
|
# Prepare Folder
|
||||||
|
# ==============
|
||||||
|
|
||||||
|
basePath = self._runtimePath
|
||||||
|
metaPath = basePath / "meta"
|
||||||
|
contPath = basePath / "content"
|
||||||
|
try:
|
||||||
|
metaPath.mkdir(exist_ok=True)
|
||||||
|
contPath.mkdir(exist_ok=True)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Failed to create project folders", exc_info=exc)
|
||||||
|
self.clear()
|
||||||
|
return NWStorageOpen.FAILED
|
||||||
|
|
||||||
|
# Check for legacy data folders
|
||||||
|
legacy = _LegacyStorage(self._project)
|
||||||
|
legacy.deprecatedFiles(basePath)
|
||||||
|
for child in basePath.iterdir():
|
||||||
|
if child.is_dir() and child.name.startswith("data_"):
|
||||||
|
legacy.legacyDataFolder(basePath, child)
|
||||||
|
|
||||||
|
self._writeLockFile()
|
||||||
|
self._ready = True
|
||||||
|
|
||||||
|
return NWStorageOpen.READY
|
||||||
|
|
||||||
def runPostSaveTasks(self, autoSave: bool = False) -> bool: # pragma: no cover
|
def runPostSaveTasks(self, autoSave: bool = False) -> bool: # pragma: no cover
|
||||||
"""Run tasks after the project has been saved.
|
"""Run tasks after the project has been saved.
|
||||||
@@ -147,7 +246,7 @@ class NWStorage:
|
|||||||
|
|
||||||
def closeSession(self) -> None:
|
def closeSession(self) -> None:
|
||||||
"""Run tasks related to closing the session."""
|
"""Run tasks related to closing the session."""
|
||||||
self.clearLockFile()
|
self._clearLockFile()
|
||||||
self.clear()
|
self.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -157,26 +256,25 @@ class NWStorage:
|
|||||||
|
|
||||||
def getXmlReader(self) -> ProjectXMLReader | None:
|
def getXmlReader(self) -> ProjectXMLReader | None:
|
||||||
"""Return a properly configured ProjectXMLReader instance."""
|
"""Return a properly configured ProjectXMLReader instance."""
|
||||||
if isinstance(self._runtimePath, Path):
|
if isinstance(self._runtimePath, Path) and self._ready:
|
||||||
projFile = self._runtimePath / nwFiles.PROJ_FILE
|
return ProjectXMLReader(self._runtimePath / nwFiles.PROJ_FILE)
|
||||||
return ProjectXMLReader(projFile)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getXmlWriter(self) -> ProjectXMLWriter | None:
|
def getXmlWriter(self) -> ProjectXMLWriter | None:
|
||||||
"""Return a properly configured ProjectXMLWriter instance."""
|
"""Return a properly configured ProjectXMLWriter instance."""
|
||||||
if isinstance(self._runtimePath, Path):
|
if isinstance(self._runtimePath, Path) and self._ready:
|
||||||
return ProjectXMLWriter(self._runtimePath)
|
return ProjectXMLWriter(self._runtimePath / nwFiles.PROJ_FILE)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getDocument(self, tHandle: str | None) -> NWDocument:
|
def getDocument(self, tHandle: str | None) -> NWDocument:
|
||||||
"""Return a document wrapper object."""
|
"""Return a document wrapper object."""
|
||||||
if isinstance(self._runtimePath, Path):
|
if isinstance(self._runtimePath, Path) and self._ready:
|
||||||
return NWDocument(self._project, tHandle)
|
return NWDocument(self._project, tHandle)
|
||||||
return NWDocument(self._project, None)
|
return NWDocument(self._project, None)
|
||||||
|
|
||||||
def getMetaFile(self, fileName: str) -> Path | None:
|
def getMetaFile(self, fileName: str) -> Path | None:
|
||||||
"""Return the path to a file in the project meta folder."""
|
"""Return the path to a file in the project meta folder."""
|
||||||
if isinstance(self._runtimePath, Path):
|
if isinstance(self._runtimePath, Path) and self._ready:
|
||||||
return self._runtimePath / "meta" / fileName
|
return self._runtimePath / "meta" / fileName
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -190,59 +288,6 @@ class NWStorage:
|
|||||||
if item.suffix == ".nwd" and isHandle(item.stem)
|
if item.suffix == ".nwd" and isHandle(item.stem)
|
||||||
] if contentPath else []
|
] if contentPath else []
|
||||||
|
|
||||||
def readLockFile(self) -> list[str]:
|
|
||||||
"""Read the project lock file."""
|
|
||||||
if self._lockFilePath is None:
|
|
||||||
return ["ERROR"]
|
|
||||||
|
|
||||||
if not self._lockFilePath.exists():
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
|
||||||
lines = self._lockFilePath.read_text(encoding="utf-8").strip().split(";")
|
|
||||||
except Exception:
|
|
||||||
logger.error("Failed to read project lockfile")
|
|
||||||
logException()
|
|
||||||
return ["ERROR"]
|
|
||||||
|
|
||||||
if len(lines) != 4:
|
|
||||||
return ["ERROR"]
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def writeLockFile(self) -> bool:
|
|
||||||
"""Write the project lock file."""
|
|
||||||
if self._lockFilePath is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = [
|
|
||||||
CONFIG.hostName, CONFIG.osType,
|
|
||||||
CONFIG.kernelVer, str(int(time()))
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
self._lockFilePath.write_text(";".join(data), encoding="utf-8")
|
|
||||||
except Exception:
|
|
||||||
logger.error("Failed to write project lockfile")
|
|
||||||
logException()
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def clearLockFile(self) -> bool:
|
|
||||||
"""Remove the lock file, if it exists."""
|
|
||||||
if self._lockFilePath is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._lockFilePath.exists():
|
|
||||||
try:
|
|
||||||
self._lockFilePath.unlink()
|
|
||||||
except Exception:
|
|
||||||
logger.error("Failed to remove project lockfile")
|
|
||||||
logException()
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def zipIt(self, target: str | Path, compression: int | None = None) -> bool:
|
def zipIt(self, target: str | Path, compression: int | None = None) -> bool:
|
||||||
"""Zip the content of the project at its runtime location into a
|
"""Zip the content of the project at its runtime location into a
|
||||||
zip file. This process will only grab files that are supposed to
|
zip file. This process will only grab files that are supposed to
|
||||||
@@ -279,7 +324,7 @@ class NWStorage:
|
|||||||
zipObj.write(srcPath, zipPath)
|
zipObj.write(srcPath, zipPath)
|
||||||
logger.debug("Added: %s", zipPath)
|
logger.debug("Added: %s", zipPath)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Failed to create acrhive")
|
logger.error("Failed to create archive")
|
||||||
logException()
|
logException()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -289,59 +334,48 @@ class NWStorage:
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _prepareStorage(self, checkLegacy: bool = True, newProject: bool = False) -> bool:
|
def _readLockFile(self) -> None:
|
||||||
"""Prepare the storage area for the project."""
|
"""Read the project lock file."""
|
||||||
path = self._runtimePath
|
self._lockedBy = None
|
||||||
if not isinstance(path, Path):
|
path = self._lockFilePath
|
||||||
logger.error("No path set")
|
if isinstance(path, Path) and path.exists():
|
||||||
self.clear()
|
try:
|
||||||
|
self._lockedBy = path.read_text(encoding="utf-8").strip().split(";")
|
||||||
|
except Exception:
|
||||||
|
logger.error("Failed to read project lockfile")
|
||||||
|
logException()
|
||||||
|
self._lockedBy = ["ERROR", "ERROR", "ERROR", "ERROR"]
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
def _writeLockFile(self) -> bool:
|
||||||
|
"""Write the project lock file."""
|
||||||
|
if self._lockFilePath is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if path == Path.home().absolute():
|
|
||||||
logger.error("Cannot use the user's home path as the root of a project")
|
|
||||||
self.clear()
|
|
||||||
return False
|
|
||||||
|
|
||||||
if newProject:
|
|
||||||
# If it's a new project, we check that there is no existing
|
|
||||||
# project in the selected path.
|
|
||||||
if path.exists() and len(list(path.iterdir())) > 0:
|
|
||||||
logger.error("The new project folder is not empty")
|
|
||||||
self.clear()
|
|
||||||
return False
|
|
||||||
|
|
||||||
# The folder is not required to exist, as it could be a new
|
|
||||||
# project, so we make sure it does. Then we add subfolders.
|
|
||||||
try:
|
try:
|
||||||
path.mkdir(exist_ok=True)
|
self._lockFilePath.write_text(
|
||||||
(path / "content").mkdir(exist_ok=True)
|
f"{CONFIG.hostName};{CONFIG.osType};{CONFIG.kernelVer};{int(time())}",
|
||||||
(path / "meta").mkdir(exist_ok=True)
|
encoding="utf-8"
|
||||||
except Exception as exc:
|
)
|
||||||
logger.error("Failed to create required project folders", exc_info=exc)
|
except Exception:
|
||||||
self.clear()
|
logger.error("Failed to write project lockfile")
|
||||||
|
logException()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not checkLegacy:
|
|
||||||
# The legacy content check is only needed for project folder
|
|
||||||
# storage, so if it is not expected to be that, there's no
|
|
||||||
# need for the remaining checks.
|
|
||||||
return True
|
|
||||||
|
|
||||||
legacy = _LegacyStorage(self._project)
|
|
||||||
|
|
||||||
# Check for legacy data folders
|
|
||||||
for child in path.iterdir():
|
|
||||||
if child.is_dir() and child.name.startswith("data_"):
|
|
||||||
legacy.legacyDataFolder(path, child)
|
|
||||||
|
|
||||||
# Check for no longer used files, and delete them
|
|
||||||
legacy.deprecatedFiles(path)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
##
|
def _clearLockFile(self) -> bool:
|
||||||
# Legacy Project Data Handlers
|
"""Remove the lock file, if it exists."""
|
||||||
##
|
if self._lockFilePath is None:
|
||||||
|
return False
|
||||||
|
if self._lockFilePath.exists():
|
||||||
|
try:
|
||||||
|
self._lockFilePath.unlink()
|
||||||
|
except Exception:
|
||||||
|
logger.error("Failed to remove project lockfile")
|
||||||
|
logException()
|
||||||
|
return False
|
||||||
|
self._lockedBy = None
|
||||||
|
return True
|
||||||
|
|
||||||
# END Class NWStorage
|
# END Class NWStorage
|
||||||
|
|
||||||
@@ -350,7 +384,7 @@ class _LegacyStorage:
|
|||||||
"""Core: Legacy Storage Converter Utils
|
"""Core: Legacy Storage Converter Utils
|
||||||
|
|
||||||
A class with various functions to convert old file formats and
|
A class with various functions to convert old file formats and
|
||||||
file/folder layout to the current project format.
|
file/folder layouts to the current project format.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, project: NWProject) -> None:
|
def __init__(self, project: NWProject) -> None:
|
||||||
@@ -359,7 +393,8 @@ class _LegacyStorage:
|
|||||||
|
|
||||||
def legacyDataFolder(self, path: Path, child: Path) -> None:
|
def legacyDataFolder(self, path: Path, child: Path) -> None:
|
||||||
"""Handle the content of a legacy data folder from a version 1.0
|
"""Handle the content of a legacy data folder from a version 1.0
|
||||||
project.
|
project. This format had 16 data folders where there now is only
|
||||||
|
one content folder.
|
||||||
"""
|
"""
|
||||||
logger.info("Processing legacy data folder: %s", path)
|
logger.info("Processing legacy data folder: %s", path)
|
||||||
|
|
||||||
@@ -411,6 +446,7 @@ class _LegacyStorage:
|
|||||||
path / "meta" / nwFiles.OPTS_FILE
|
path / "meta" / nwFiles.OPTS_FILE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Delete removed files
|
||||||
remove = [
|
remove = [
|
||||||
path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1
|
path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1
|
||||||
path / "meta" / "mainOptions.json", # Replaced in 0.5
|
path / "meta" / "mainOptions.json", # Replaced in 0.5
|
||||||
@@ -457,7 +493,6 @@ class _LegacyStorage:
|
|||||||
|
|
||||||
# Save dictionary and clean up old file
|
# Save dictionary and clean up old file
|
||||||
userDict.save()
|
userDict.save()
|
||||||
assert wordJson.exists()
|
|
||||||
wordList.unlink()
|
wordList.unlink()
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -467,9 +502,7 @@ class _LegacyStorage:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> None:
|
def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> None:
|
||||||
"""Convert the old text log file format to the new JSON Lines
|
"""Convert the old text log file format to JSON Lines."""
|
||||||
format.
|
|
||||||
"""
|
|
||||||
if sessJson.exists() or not sessLog.exists():
|
if sessJson.exists() or not sessLog.exists():
|
||||||
# If the new file already exists, we won't overwrite it
|
# If the new file already exists, we won't overwrite it
|
||||||
return
|
return
|
||||||
@@ -508,7 +541,7 @@ class _LegacyStorage:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def _convertOldOptionsFile(self, optsOld: Path, optsNew: Path) -> None:
|
def _convertOldOptionsFile(self, optsOld: Path, optsNew: Path) -> None:
|
||||||
"""Convert the old options state file format to the format."""
|
"""Convert the old options state file format to new format."""
|
||||||
if optsNew.exists() or not optsOld.exists():
|
if optsNew.exists() or not optsOld.exists():
|
||||||
# If the new file already exists, we won't overwrite it
|
# If the new file already exists, we won't overwrite it
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user