Move zipping of project to storage class

This commit is contained in:
Veronica Berglyd Olsen
2022-11-08 19:25:56 +01:00
parent 262a6c36b7
commit b024c0996d
8 changed files with 96 additions and 95 deletions
+21 -56
View File
@@ -27,7 +27,6 @@ from __future__ import annotations
import os
import json
import shutil
import logging
import novelwriter
@@ -299,7 +298,6 @@ class NWProject(QObject):
xmlParsed = xmlReader.read(self._data, projContent)
appVersion = xmlReader.appVersion or self.tr("Unknown")
hexVersion = xmlReader.hexVersion or "0x0"
if not xmlParsed:
if xmlReader.state == XMLReadState.NOT_NWX_FILE:
@@ -339,7 +337,7 @@ class NWProject(QObject):
# Check novelWriter Version
# =========================
if hexToInt(hexVersion) > hexToInt(novelwriter.__hexversion__):
if xmlReader.hexVersion > hexToInt(novelwriter.__hexversion__):
msgYes = self.mainGui.askQuestion(
self.tr("Version Conflict"),
self.tr(
@@ -359,7 +357,7 @@ class NWProject(QObject):
self._tree.unpack(projContent)
self._options.loadSettings()
self._index.loadIndex()
self._loadProjectLocalisation()
# Update recent projects
self.mainConf.updateRecentCache(
@@ -376,7 +374,7 @@ class NWProject(QObject):
del self._tree[tHandle] # The file will be re-added as orphaned
self._scanProjectFolder()
self._loadProjectLocalisation()
self._index.loadIndex()
self.updateWordCounts()
self._projOpened = time()
@@ -466,21 +464,17 @@ class NWProject(QObject):
self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
return
##
# Zip/Unzip Project
##
def zipIt(self, doNotify):
def backupProject(self, doNotify):
"""Create a zip file of the entire project.
"""
if not self.mainGui.hasProject:
if not self._storage.isOpen():
logger.error("No project open")
return False
logger.info("Backing up project")
self.mainGui.setStatus(self.tr("Backing up project ..."))
if not (self.mainConf.backupPath and os.path.isdir(self.mainConf.backupPath)):
if not self.mainConf.backupPath:
self.mainGui.makeAlert(self.tr(
"Cannot backup project because no valid backup path is set. "
"Please set a valid backup location in Preferences."
@@ -490,52 +484,37 @@ class NWProject(QObject):
if not self._data.name:
self.mainGui.makeAlert(self.tr(
"Cannot backup project because no project name is set. "
"Please set a Working Title in Project Settings."
"Please set a Project Name in Project Settings."
), nwAlert.ERROR)
return False
cleanName = makeFileNameSafe(self._data.name)
baseDir = os.path.abspath(os.path.join(self.mainConf.backupPath, cleanName))
if not os.path.isdir(baseDir):
try:
os.mkdir(baseDir)
logger.debug("Created folder: %s", baseDir)
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Could not create backup folder."
), nwAlert.ERROR, exception=exc)
return False
if baseDir and baseDir.startswith(str(self._storage.runtimePath)):
baseDir = Path(self.mainConf.backupPath) / cleanName
try:
baseDir.mkdir(exist_ok=True)
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Cannot backup project because the backup path is within the "
"project folder to be backed up. Please choose a different "
"backup path in Preferences."
), nwAlert.ERROR)
"Could not create backup folder."
), nwAlert.ERROR, exception=exc)
return False
archName = self.tr("Backup from {0}").format(formatTimeStamp(time(), fileSafe=True))
baseName = os.path.join(baseDir, archName)
try:
self._storage.clearLockFile()
shutil.make_archive(baseName, "zip", self._storage.runtimePath, ".")
self._storage.writeLockFile()
logger.info("Backup written to: %s", archName)
archName = baseDir / self.tr(
"Backup from {0}.zip"
).format(formatTimeStamp(time(), fileSafe=True))
if self._storage.zipIt(archName, compression=2):
if doNotify:
self.mainGui.makeAlert(self.tr(
"Backup archive file written to: {0}"
).format(f"{os.path.join(cleanName, archName)}.zip"), nwAlert.INFO)
except Exception as exc:
).format(str(archName), nwAlert.INFO))
else:
self.mainGui.makeAlert(self.tr(
"Could not write backup archive."
), nwAlert.ERROR, exception=exc)
), nwAlert.ERROR)
return False
self.mainGui.setStatus(self.tr(
"Project backed up to '{0}'"
).format(f"{baseName}.zip"))
).format(str(archName)))
return True
@@ -737,20 +716,6 @@ class NWProject(QObject):
return True
def _checkFolder(self, thePath):
"""Check if a folder exists, and if it doesn't, create it.
"""
if not os.path.isdir(thePath):
try:
os.mkdir(thePath)
logger.debug("Created folder: %s", thePath)
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Could not create folder."
), nwAlert.ERROR, exception=exc)
return False
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
+4 -4
View File
@@ -34,7 +34,7 @@ from pathlib import Path
from novelwriter.common import (
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
simplified, yesNo
hexToInt, simplified, yesNo
)
from novelwriter.constants import nwFiles
@@ -104,9 +104,9 @@ class ProjectXMLReader:
self._state = XMLReadState.NO_ACTION
self._root = ""
self._version = 0x0000
self._version = 0x0
self._appVersion = ""
self._hexVersion = ""
self._hexVersion = 0x0
self._timeStamp = ""
return
@@ -200,7 +200,7 @@ class ProjectXMLReader:
logger.debug("XML is '%s' version '%s'", self._root, fileVersion)
self._appVersion = str(xRoot.attrib.get("appVersion", ""))
self._hexVersion = str(xRoot.attrib.get("hexVersion", ""))
self._hexVersion = hexToInt(xRoot.attrib.get("hexVersion", ""))
self._timeStamp = str(xRoot.attrib.get("timeStamp", ""))
for xSection in xRoot:
+50 -11
View File
@@ -28,7 +28,9 @@ import novelwriter
from time import time
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
from novelwriter.common import minmax
from novelwriter.constants import nwFiles
from novelwriter.core.document import NWDocument
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
@@ -232,18 +234,55 @@ class NWStorage:
return True
def zipIt(self, target, compression=None):
"""Zip the content of the project at its runtime location into a
zip file. This process will only grab files that are supposed to
be in the project. All non-project files will be left out.
"""
basePath = self._runtimePath
if not isinstance(basePath, Path):
logger.error("No path set")
return False
baseMeta = basePath / "meta"
baseCont = basePath / "content"
files = [
(basePath / nwFiles.PROJ_FILE, nwFiles.PROJ_FILE),
(baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
(baseMeta / nwFiles.SESS_STATS, f"meta/{nwFiles.SESS_STATS}"),
(baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
(baseMeta / nwFiles.PROJ_DICT, f"meta/{nwFiles.PROJ_DICT}"),
]
for contItem in baseCont.iterdir():
name = contItem.name
if contItem.is_file() and len(name) == 17 and name.endswith(".nwd"):
files.append((contItem, f"content/{name}"))
comp = ZIP_STORED if compression is None else ZIP_DEFLATED
level = minmax(compression, 0, 9) if isinstance(compression, int) else None
try:
with ZipFile(target, mode="w", compression=comp, compresslevel=level) as zipObj:
logger.info("Creating archive: %s", target)
for srcPath, zipPath in files:
if srcPath.is_file():
zipObj.write(srcPath, zipPath)
logger.debug("Added: %s", zipPath)
except Exception:
logger.error("Failed to create acrhive")
logException()
return False
return True
##
# Internal Functions
##
def _zipIt(self, target):
pass
def _prepareStorage(self, checkLegacy=True, newProject=False):
"""Prepare the storage area for the project.
"""
path = self._runtimePath
if path is None:
if not isinstance(path, Path):
logger.error("No path set")
self.clear()
return False
@@ -336,13 +375,13 @@ class NWStorage:
"""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",
path / "meta" / "mainOptions.json", # Replaced in 0.5
path / "meta" / "exportOptions.json", # Replaced in 0.5
path / "meta" / "outlineOptions.json", # Replaced in 0.5
path / "meta" / "timelineOptions.json", # Replaced in 0.5
path / "meta" / "docMergeOptions.json", # Replaced in 0.5
path / "meta" / "sessionLogOptions.json", # Replaced in 0.5
path / "ToC.json", # Dropped in 1.0 RC 1
]
for item in remove:
if item.is_file():
+1 -1
View File
@@ -824,7 +824,7 @@ class GuiMainMenu(QMenuBar):
# Tools > Backup
self.aBackupProject = QAction(self.tr("Backup Project"), self)
self.aBackupProject.triggered.connect(lambda: self.theProject.zipIt(True))
self.aBackupProject.triggered.connect(lambda: self.theProject.backupProject(doNoify=True))
self.toolsMenu.addAction(self.aBackupProject)
# Tools > Export Project
+1 -1
View File
@@ -409,7 +409,7 @@ class GuiMain(QMainWindow):
if not msgYes:
doBackup = False
if doBackup:
self.theProject.zipIt(False)
self.theProject.backupProject(doNotify=False)
else:
saveOK = True
+2 -1
View File
@@ -29,6 +29,7 @@ from mock import causeException
from tools import C, buildTestProject, cmpFiles, writeFile
from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.constants import nwFiles
from novelwriter.core.index import NWIndex, countWords, TagsIndex
from novelwriter.core.project import NWProject
@@ -38,7 +39,7 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir):
"""Test core functionality of scaning, saving, loading and checking
the index cache file.
"""
projFile = os.path.join(nwLipsum, "meta", "tagsIndex.json")
projFile = os.path.join(nwLipsum, "meta", nwFiles.INDEX_FILE)
testFile = os.path.join(outDir, "coreIndex_LoadSave_tagsIndex.json")
compFile = os.path.join(refDir, "coreIndex_LoadSave_tagsIndex.json")
+11 -15
View File
@@ -235,7 +235,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
# Won't convert legacy file
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: "0x99999999"))
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999))
mockGUI.askResponse = False
assert theProject.openProject(fncDir) is False
assert "This project was saved by a newer version" in mockGUI.lastQuestion[1]
@@ -699,46 +699,42 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncDir, tmpDir):
# No project
mockGUI.hasProject = False
assert theProject.zipIt(doNotify=False) is False
assert theProject.backupProject(doNotify=False) is False
mockGUI.hasProject = True
# Invalid path
theProject.mainConf.backupPath = None
assert theProject.zipIt(doNotify=False) is False
assert theProject.backupProject(doNotify=False) is False
# Missing project name
theProject.mainConf.backupPath = tmpDir
theProject.data.setName("")
assert theProject.zipIt(doNotify=False) is False
assert theProject.backupProject(doNotify=False) is False
# Non-existent folder
theProject.mainConf.backupPath = os.path.join(tmpDir, "nonexistent")
theProject.data.setName("Test Minimal")
assert theProject.zipIt(doNotify=False) is False
# Same folder as project (causes infinite loop in zipping)
theProject.mainConf.backupPath = fncDir
assert theProject.zipIt(doNotify=False) is False
assert theProject.backupProject(doNotify=False) is False
# Subfolder of project (causes infinite loop in zipping)
theProject.mainConf.backupPath = os.path.join(fncDir, "subdir")
assert theProject.zipIt(doNotify=False) is False
assert theProject.backupProject(doNotify=False) is False
# Set a valid folder
theProject.mainConf.backupPath = tmpDir
# Can't make folder
with monkeypatch.context() as mp:
mp.setattr("os.mkdir", causeOSError)
assert theProject.zipIt(doNotify=False) is False
mp.setattr("pathlib.Path.mkdir", causeOSError)
assert theProject.backupProject(doNotify=False) is False
# Can't write archive
with monkeypatch.context() as mp:
mp.setattr("shutil.make_archive", causeOSError)
assert theProject.zipIt(doNotify=False) is False
mp.setattr("zipfile.ZipFile.write", causeOSError)
assert theProject.backupProject(doNotify=False) is False
# Test correct settings
assert theProject.zipIt(doNotify=True) is True
assert theProject.backupProject(doNotify=True) is True
theFiles = os.listdir(os.path.join(tmpDir, "Test Minimal"))
assert len(theFiles) == 1
+6 -6
View File
@@ -131,7 +131,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0105
assert xmlReader.appVersion == "2.0-rc1"
assert xmlReader.hexVersion == "0x020000c1"
assert xmlReader.hexVersion == 0x020000c1
# Check loaded data
assert data.name == "Sample Project"
@@ -257,7 +257,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0100
assert xmlReader.appVersion == "0.6.1"
assert xmlReader.hexVersion == "0x000601f0"
assert xmlReader.hexVersion == 0x000601f0
# Check loaded data
assert data.name == "Sample Project"
@@ -399,7 +399,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0101
assert xmlReader.appVersion == "0.9.2"
assert xmlReader.hexVersion == "0x000902f0"
assert xmlReader.hexVersion == 0x000902f0
# Check loaded data
assert data.name == "Sample Project"
@@ -541,7 +541,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0102
assert xmlReader.appVersion == "1.4.2"
assert xmlReader.hexVersion == "0x010402f0"
assert xmlReader.hexVersion == 0x010402f0
# Check loaded data
assert data.name == "Sample Project"
@@ -686,7 +686,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0103
assert xmlReader.appVersion == "1.6.6"
assert xmlReader.hexVersion == "0x010606f0"
assert xmlReader.hexVersion == 0x010606f0
# Check loaded data
assert data.name == "Sample Project"
@@ -831,7 +831,7 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0104
assert xmlReader.appVersion == "2.0-rc1"
assert xmlReader.hexVersion == "0x020000c1"
assert xmlReader.hexVersion == 0x020000c1
# Check loaded data
assert data.name == "Sample Project"