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