Fix missing data path issue (#1180)

This commit is contained in:
Veronica Berglyd Olsen
2022-10-18 20:43:33 +02:00
parent d232b22501
commit 33b95b60e3
5 changed files with 90 additions and 123 deletions
+20 -1
View File
@@ -35,7 +35,7 @@ from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import qApp from PyQt5.QtWidgets import qApp
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import formatException, logException
from novelwriter.constants import nwConst, nwUnicode from novelwriter.constants import nwConst, nwUnicode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -475,6 +475,25 @@ def makeFileNameSafe(value):
return cleanName return cleanName
def ensureFolder(dirPath, parentPath=None, errLog=None):
"""Make sure a folder exists, and if it doesn't, create it.
"""
try:
if parentPath:
dirPath = os.path.join(parentPath, dirPath)
if not os.path.isdir(dirPath):
os.mkdir(dirPath)
except Exception as exc:
logger.error("Could not create folder: %s", dirPath)
logException()
if isinstance(errLog, list):
errLog.append(f"Could not create folder: {dirPath}")
errLog.append(formatException(exc))
return False
return True
def sha256sum(filePath): def sha256sum(filePath):
"""Make a shasum of a file using a buffer. """Make a shasum of a file using a buffer.
Based on: https://stackoverflow.com/a/44873382/5825851 Based on: https://stackoverflow.com/a/44873382/5825851
+38 -87
View File
@@ -37,7 +37,7 @@ from PyQt5.QtCore import (
) )
from novelwriter.error import logException, formatException from novelwriter.error import logException, formatException
from novelwriter.common import splitVersionNumber, formatTimeStamp, NWConfigParser from novelwriter.common import ensureFolder, splitVersionNumber, formatTimeStamp, NWConfigParser
from novelwriter.constants import nwFiles, nwUnicode from novelwriter.constants import nwFiles, nwUnicode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -54,23 +54,21 @@ class Config:
self.appName = "novelWriter" self.appName = "novelWriter"
self.appHandle = self.appName.lower() self.appHandle = self.appName.lower()
# Config Error Handling
self.hasError = False # True if the config class encountered an error
self.errData = [] # List of error messages
# Set Paths # Set Paths
self.cmdOpen = None # Path from command line for project to be opened on launch self.cmdOpen = None # Path from command line for project to be opened on launch
self.confPath = None # Folder where the config is saved self.confPath = None # Folder where the config is saved
self.confFile = None # The config file name self.confFile = None # The config file name
self.dataPath = None # Folder where app data is stored self.dataPath = None # Folder where app data is stored
self.lastPath = None # The last user-selected folder (browse dialogs) self.lastPath = None # The last user-selected folder (browse dialogs)
self.appPath = None # The full path to the novelwriter package folder self.appPath = None # The full path to the novelwriter package folder
self.appRoot = None # The full path to the novelwriter root folder self.appRoot = None # The full path to the novelwriter root folder
self.appIcon = None # The full path to the novelwriter icon file self.appIcon = None # The full path to the novelwriter icon file
self.assetPath = None # The full path to the novelwriter/assets folder self.assetPath = None # The full path to the novelwriter/assets folder
self.pdfDocs = None # The location of the PDF manual, if it exists self.pdfDocs = None # The location of the PDF manual, if it exists
# Runtime Settings and Variables # Runtime Settings and Variables
self.hasError = False # True if the config class encountered an error
self.errData = [] # List of error messages
self.confChanged = False # True whenever the config has chenged, false after save self.confChanged = False # True whenever the config has chenged, false after save
# General # General
@@ -94,14 +92,14 @@ class Config:
self.qtTrans = {} self.qtTrans = {}
# Sizes # Sizes
self.winGeometry = [1200, 650] self.winGeometry = [1200, 650]
self.prefGeometry = [700, 615] self.prefGeometry = [700, 615]
self.projColWidth = [200, 60, 140] self.projColWidth = [200, 60, 140]
self.mainPanePos = [300, 800] self.mainPanePos = [300, 800]
self.docPanePos = [400, 400] self.docPanePos = [400, 400]
self.viewPanePos = [500, 150] self.viewPanePos = [500, 150]
self.outlnPanePos = [500, 150] self.outlnPanePos = [500, 150]
self.isFullScreen = False self.isFullScreen = False
# Features # Features
self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideVScroll = False # Hide vertical scroll bars on main widgets
@@ -270,20 +268,8 @@ class Config:
logger.info("Setting data path from alternative path: %s", dataPath) logger.info("Setting data path from alternative path: %s", dataPath)
self.dataPath = dataPath self.dataPath = dataPath
logger.verbose("Config path: %s", self.confPath) logger.debug("Config path: %s", self.confPath)
logger.verbose("Data path: %s", self.dataPath) logger.debug("Data path: %s", self.dataPath)
# Check Data Path Subdirs
dataDirs = ["syntax", "themes"]
for dataDir in dataDirs:
dirPath = os.path.join(self.dataPath, dataDir)
if not os.path.isdir(dirPath):
try:
os.mkdir(dirPath)
logger.info("Created folder: %s", dirPath)
except Exception:
logger.error("Could not create folder: %s", dirPath)
logException()
self.confFile = self.appHandle+".conf" self.confFile = self.appHandle+".conf"
self.lastPath = os.path.expanduser("~") self.lastPath = os.path.expanduser("~")
@@ -308,18 +294,20 @@ class Config:
logger.verbose("App path: %s", self.appPath) logger.verbose("App path: %s", self.appPath)
logger.verbose("Last path: %s", self.lastPath) logger.verbose("Last path: %s", self.lastPath)
# If the config folder does not exist, create it. # If the config and data folders don't not exist, create them
# This assumes that the os config folder itself exists. # This assumes that the os config and data folders exist
if not os.path.isdir(self.confPath): if not ensureFolder(self.confPath, errLog=self.errData):
try: self.hasError = True
os.mkdir(self.confPath) self.confPath = None
except Exception as exc:
logger.error("Could not create folder: %s", self.confPath) if not ensureFolder(self.dataPath, errLog=self.errData):
logException() self.hasError = True
self.hasError = True self.dataPath = None
self.errData.append("Could not create folder: %s" % self.confPath)
self.errData.append(formatException(exc)) # We don't error on these failing since they are not essential
self.confPath = None if self.dataPath is not None:
ensureFolder("syntax", parentPath=self.dataPath)
ensureFolder("themes", parentPath=self.dataPath)
# Check if config file exists # Check if config file exists
if self.confPath is not None: if self.confPath is not None:
@@ -330,20 +318,6 @@ class Config:
# If it does not exist, save a copy of the default values # If it does not exist, save a copy of the default values
self.saveConfig() self.saveConfig()
# If the data folder does not exist, create it.
# This assumes that the os data folder itself exists.
if self.dataPath is not None:
if not os.path.isdir(self.dataPath):
try:
os.mkdir(self.dataPath)
except Exception as exc:
logger.error("Could not create folder: %s", self.dataPath)
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.dataPath)
self.errData.append(formatException(exc))
self.dataPath = None
# Load recent projects cache # Load recent projects cache
self.loadRecentCache() self.loadRecentCache()
@@ -389,7 +363,7 @@ class Config:
def listLanguages(self, lngSet): def listLanguages(self, lngSet):
"""List localisation files in the i18n folder. The default GUI """List localisation files in the i18n folder. The default GUI
language 'en_GB' is British English. language is British English (en_GB).
""" """
if lngSet == self.LANG_NW: if lngSet == self.LANG_NW:
fPre = "nw_" fPre = "nw_"
@@ -741,29 +715,6 @@ class Config:
# Setters # Setters
## ##
def setConfPath(self, newPath):
"""Set the path and filename to the config file.
"""
if newPath is None:
return True
if not os.path.isfile(newPath):
logger.error("File not found, using default config path instead")
return False
self.confPath = os.path.dirname(newPath)
self.confFile = os.path.basename(newPath)
return True
def setDataPath(self, newPath):
"""Set the data path.
"""
if newPath is None:
return True
if not os.path.isdir(newPath):
logger.error("Path not found, using default data path instead")
return False
self.dataPath = os.path.abspath(newPath)
return True
def setLastPath(self, lastPath): def setLastPath(self, lastPath):
"""Set the last used path (by the user). """Set the last used path (by the user).
""" """
+4 -2
View File
@@ -120,11 +120,13 @@ class GuiTheme:
self._availThemes = {} self._availThemes = {}
self._availSyntax = {} self._availSyntax = {}
self._listConf(self._availSyntax, os.path.join(self.mainConf.dataPath, "syntax"))
self._listConf(self._availSyntax, os.path.join(self.mainConf.assetPath, "syntax")) self._listConf(self._availSyntax, os.path.join(self.mainConf.assetPath, "syntax"))
self._listConf(self._availThemes, os.path.join(self.mainConf.dataPath, "themes"))
self._listConf(self._availThemes, os.path.join(self.mainConf.assetPath, "themes")) self._listConf(self._availThemes, os.path.join(self.mainConf.assetPath, "themes"))
if self.mainConf.dataPath: # Not guaranteed to be set
self._listConf(self._availSyntax, os.path.join(self.mainConf.dataPath, "syntax"))
self._listConf(self._availThemes, os.path.join(self.mainConf.dataPath, "themes"))
self.updateFont() self.updateFont()
self.updateTheme() self.updateTheme()
self.iconCache.updateTheme() self.iconCache.updateTheme()
+28 -1
View File
@@ -33,7 +33,8 @@ from novelwriter.common import (
isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, checkIntRange, isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, checkIntRange,
minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified, minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified,
splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode, splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode,
readTextFile, makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser readTextFile, makeFileNameSafe, ensureFolder, sha256sum, getGuiItem,
NWConfigParser
) )
@@ -541,6 +542,32 @@ def testBaseCommon_MakeFileNameSafe():
# END Test testBaseCommon_MakeFileNameSafe # END Test testBaseCommon_MakeFileNameSafe
@pytest.mark.base
def testBaseCommon_EnsureFolder(monkeypatch, fncDir):
"""Test the ensureFolder function.
"""
newDir1 = os.path.join(fncDir, "newDir1")
newDir2 = os.path.join(fncDir, "newDir2")
newDir3 = os.path.join(fncDir, "newDir3")
assert ensureFolder(None) is False
assert ensureFolder(newDir1) is True
assert os.path.isdir(newDir1)
assert ensureFolder("newDir2", parentPath=fncDir) is True
assert os.path.isdir(newDir2)
with monkeypatch.context() as mp:
mp.setattr("os.mkdir", causeOSError)
errLog = []
assert ensureFolder("newDir3", parentPath=fncDir, errLog=errLog) is False
assert errLog[0] == f"Could not create folder: {newDir3}"
assert not os.path.isdir(newDir3)
# END Test testBaseCommon_EnsureFolder
@pytest.mark.base @pytest.mark.base
def testBaseCommon_Sha256Sum(monkeypatch, fncDir, ipsumText): def testBaseCommon_Sha256Sum(monkeypatch, fncDir, ipsumText):
"""Test the sha256sum function. """Test the sha256sum function.
-32
View File
@@ -316,38 +316,6 @@ def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir):
# END Test testBaseConfig_RecentCache # END Test testBaseConfig_RecentCache
@pytest.mark.base
def testBaseConfig_SetPath(tmpConf, tmpDir):
"""Test path setters.
"""
# Conf Path
assert tmpConf.setConfPath(None)
assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setConfPath(os.path.join(tmpDir, "novelwriter.conf"))
assert tmpConf.confPath == tmpDir
assert tmpConf.confFile == "novelwriter.conf"
assert not tmpConf.confChanged
# Data Path
assert tmpConf.setDataPath(None)
assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setDataPath(tmpDir)
assert tmpConf.dataPath == tmpDir
assert not tmpConf.confChanged
# Last Path
assert tmpConf.setLastPath(None)
assert tmpConf.lastPath == ""
assert tmpConf.setLastPath(os.path.join(tmpDir, "file.tmp"))
assert tmpConf.lastPath == tmpDir
assert tmpConf.setLastPath("")
assert tmpConf.lastPath == ""
# END Test testBaseConfig_SetPath
@pytest.mark.base @pytest.mark.base
def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
"""Set various sizes and positions """Set various sizes and positions