Add storage code to handle projecty structure and old projects

This commit is contained in:
Veronica Berglyd Olsen
2022-11-05 16:15:26 +01:00
parent 2e590b742f
commit 407e7fa69e
4 changed files with 154 additions and 62 deletions
+4 -4
View File
@@ -3707,8 +3707,8 @@ helpful feedback and issue reports for the new features added in this, and previ
**User Interface**
* Added a preferences dialog for the program settings. No longer necessary to edit the config file.
PR #30.
* Added a preferences dialog for the program settings. It is no longer necessary to edit the config
file. PR #30.
* The document viewer remembers scroll bar position when pressing `Ctrl+R` on a document already
being viewed. PR #28.
* Removed version number from windows title. PR #28.
@@ -3749,8 +3749,8 @@ helpful feedback and issue reports for the new features added in this, and previ
**Status Bar**
* Redesign of the status bar adding project and session stats as well as a session timer. PR #21.
* Project word count is written to the project file, which is needed for the session word count. PR
#21.
* Project word count is written to the project file, which is needed for the session word count.
PR #21.
* Closing a project now clears the status bar. PR #21.
**Editor**
+10 -41
View File
@@ -264,42 +264,26 @@ class NWProject(QObject):
return
def openProject(self, fileName, overrideLock=False):
def openProject(self, projPath, overrideLock=False):
"""Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful,
parse the XML of the file and populate the project variables and
build the tree of project items.
"""
if not os.path.isfile(fileName):
fileName = os.path.join(fileName, nwFiles.PROJ_FILE)
if not os.path.isfile(fileName):
self.mainGui.makeAlert(self.tr(
"File not found: {0}"
).format(fileName), nwAlert.ERROR)
return False
self.clearProject()
self.projPath = os.path.abspath(os.path.dirname(fileName))
logger.info("Opening project: %s", self.projPath)
# Standard Folders and Files
# ==========================
if not self.ensureFolderStructure():
self.clearProject()
if not self._storage.openProjectInPlace(projPath):
return False
# ToDo: These should not be set explicitly, and should stay as Path
self.projPath = str(self._storage.runtimePath)
self.projContent = str(self._storage.contentPath)
self.projCache = str(self._storage.cachePath)
self.projMeta = str(self._storage.metaPath)
logger.info("Opening project: %s", self.projPath)
self.projDict = os.path.join(self.projMeta, nwFiles.PROJ_DICT)
# Check for Old Legacy Data
# =========================
legacyList = [] # Cleanup is done later
for projItem in os.listdir(self.projPath):
logger.debug("Project contains: %s", projItem)
if projItem.startswith("data_") and len(projItem) == 6:
legacyList.append(projItem)
# Project Lock
# ============
@@ -321,10 +305,6 @@ 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()
@@ -398,17 +378,6 @@ class NWProject(QObject):
self._options.loadSettings()
self._index.loadIndex()
# Sort out old file locations
if legacyList:
try:
for projItem in legacyList:
self._legacyDataFolder(projItem)
except Exception:
self.mainGui.makeAlert(self.tr(
"There was an error updating the project. "
"Some data may not have been preserved."
), nwAlert.ERROR)
# Clean up no longer used files
self._deprecatedFiles()
+140 -7
View File
@@ -57,6 +57,32 @@ class NWStorage:
self._openMode = self.MODE_INACTIVE
return
##
# Properties
##
@property
def runtimePath(self):
return self._runtimePath
@property
def contentPath(self):
if self._runtimePath is not None:
return self._runtimePath / "content"
return None
@property
def metaPath(self):
if self._runtimePath is not None:
return self._runtimePath / "meta"
return None
@property
def cachePath(self):
if self._runtimePath is not None:
return self._runtimePath / "cache"
return None
##
# Core Methods
##
@@ -70,19 +96,20 @@ class NWStorage:
"""Open a novelWriter project in-place. That is, it is opened
directly from a project folder.
"""
inPath = Path(path)
inPath = Path(path).resolve()
if inPath.is_file():
# The path should not point to an exisitng file,
# but it can point to a folder containing files
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
if self._prepareStorage(checkLegacy=True) is False:
self.clear()
return False
return True
def openProjectArchive(self, path):
@@ -142,10 +169,116 @@ class NWStorage:
def _zipIt(self, target):
pass
def _reeadLockFile(self):
def _readLockFile(self):
pass
def _writeLockFile(self):
pass
def _prepareStorage(self, checkLegacy=True):
"""Prepare the storage area for the project.
"""
path = self._runtimePath
if path is None:
logger.error("No path set")
self.clear()
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
# 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:
path.mkdir(exist_ok=True)
(path / "content").mkdir(exist_ok=True)
(path / "cache").mkdir(exist_ok=True)
(path / "meta").mkdir(exist_ok=True)
except Exception as exc:
logger.error("Failed to create required project folders", exc_info=exc)
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 remaning checks.
return True
# Check for legacy data folders
for child in path.iterdir():
if child.is_dir() and child.name.startswith("data_"):
self._legacyDataFolder(path, child)
# Check for no longer used files, and delete them
self._deleteDeprecatedFiles(path)
return True
##
# Legacy Project Data Handlers
##
def _legacyDataFolder(self, path: Path, child: Path):
"""Handle the content of a legacy data folder from a version 1.0
project.
"""
logger.info("Processing legacy data folder: %s", path)
# Move Documents to Content
first = child.name[-1]
if first not in "0123456789abcdef":
return
for item in child.iterdir():
if not item.is_file():
continue
name = item.name
if len(name) == 21 and name.endswith("_main.nwd"):
newPath = path / "content" / f"{first}{name[:12]}.nwd"
try:
item.rename(newPath)
logger.info("Moved file: %s", newPath)
except Exception as exc:
logger.warning("Failed to move: %s", item, exc_info=exc)
elif len(name) == 21 and name.endswith("_main.bak"):
try:
item.unlink()
logger.info("Deleted file: %s", item)
except Exception as exc:
logger.warning("Failed to delete: %s", item, exc_info=exc)
# Remove Data Folder
try:
child.rmdir()
logger.info("Deleted folder: %s", child)
except Exception as exc:
logger.warning("Failed to delete: %s", child, exc_info=exc)
return
def _deleteDeprecatedFiles(self, path: Path):
"""Delete files that are no longer used by novelWriter.
"""
remove = [
path / "meta" / "mainOptions.json",
path / "meta" / "exportOptions.json",
path / "meta" / "outlineOptions.json",
path / "meta" / "timelineOptions.json",
path / "meta" / "docMergeOptions.json",
path / "meta" / "sessionLogOptions.json",
path / "ToC.json",
]
for item in remove:
if item.is_file():
try:
item.unlink()
logger.info("Deleted: %s", item)
except Exception as exc:
logger.warning("Failed to delete: %s", item, exc_info=exc)
return
# END Class NWStorage
-10
View File
@@ -250,16 +250,6 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
assert "This project was saved by a newer version" in mockGUI.lastQuestion[1]
mockGUI.askResponse = True
# Add some legacy stuff that cannot be removed
with monkeypatch.context() as mp:
mp.setattr(theProject, "_legacyDataFolder", causeOSError)
os.mkdir(os.path.join(fncDir, "data_0"))
writeFile(os.path.join(fncDir, "data_0", "123456789abc_main.nwd"), "stuff")
writeFile(os.path.join(fncDir, "data_0", "123456789abc_main.bak"), "stuff")
mockGUI.clear()
assert theProject.openProject(fncDir) is True
assert "There was an error updating the project." in mockGUI.lastAlert
assert theProject.closeProject()
# END Test testCoreProject_Open