Merge pull request #165 from vkbo/file_save_update
File saving and opening
This commit is contained in:
@@ -22,7 +22,6 @@ class nwFiles():
|
|||||||
|
|
||||||
APP_ICON = "novelWriter.svg"
|
APP_ICON = "novelWriter.svg"
|
||||||
PROJ_FILE = "nwProject.nwx"
|
PROJ_FILE = "nwProject.nwx"
|
||||||
PROJ_COUNT = "projCount.txt"
|
|
||||||
PROJ_DICT = "wordlist.txt"
|
PROJ_DICT = "wordlist.txt"
|
||||||
SESS_INFO = "sessionInfo.log"
|
SESS_INFO = "sessionInfo.log"
|
||||||
INDEX_FILE = "tagsIndex.json"
|
INDEX_FILE = "tagsIndex.json"
|
||||||
|
|||||||
+3
-3
@@ -330,7 +330,7 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def saveProject(self, isAuto=False):
|
def saveProject(self):
|
||||||
"""Save the current project.
|
"""Save the current project.
|
||||||
"""
|
"""
|
||||||
if not self.hasProject:
|
if not self.hasProject:
|
||||||
@@ -344,7 +344,7 @@ class GuiMain(QMainWindow):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
self.treeView.saveTreeOrder()
|
self.treeView.saveTreeOrder()
|
||||||
self.theProject.saveProject(isAuto)
|
self.theProject.saveProject()
|
||||||
self.theIndex.saveIndex()
|
self.theIndex.saveIndex()
|
||||||
self.mainMenu.updateRecentProjects()
|
self.mainMenu.updateRecentProjects()
|
||||||
|
|
||||||
@@ -840,7 +840,7 @@ class GuiMain(QMainWindow):
|
|||||||
if (self.hasProject and self.theProject.projChanged and
|
if (self.hasProject and self.theProject.projChanged and
|
||||||
self.theProject.projPath is not None):
|
self.theProject.projPath is not None):
|
||||||
logger.debug("Autosaving project")
|
logger.debug("Autosaving project")
|
||||||
self.saveProject(isAuto=True)
|
self.saveProject()
|
||||||
return
|
return
|
||||||
|
|
||||||
def _autoSaveDocument(self):
|
def _autoSaveDocument(self):
|
||||||
|
|||||||
+13
-15
@@ -100,25 +100,23 @@ class NWDoc():
|
|||||||
mkdir(dataPath)
|
mkdir(dataPath)
|
||||||
logger.debug("Created folder %s" % dataPath)
|
logger.debug("Created folder %s" % dataPath)
|
||||||
|
|
||||||
docTemp = path.join(dataPath,docFile[:-3]+"tmp")
|
docTemp = path.join(dataPath, docFile+"~")
|
||||||
docBack = path.join(dataPath,docFile[:-3]+"bak")
|
docBack = path.join(dataPath, docFile[:-3]+"bak")
|
||||||
|
|
||||||
if path.isfile(docTemp):
|
|
||||||
unlink(docTemp)
|
|
||||||
if path.isfile(docBack):
|
|
||||||
rename(docBack,docTemp)
|
|
||||||
if path.isfile(docPath):
|
|
||||||
rename(docPath,docBack)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(docPath,mode="w",encoding="utf8") as outFile:
|
with open(docTemp,mode="w",encoding="utf8") as outFile:
|
||||||
outFile.write(docText)
|
outFile.write(docText)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR)
|
self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if path.isfile(docTemp):
|
# If we're here, the file was successfully saved,
|
||||||
unlink(docTemp)
|
# so let's sort out the temps and backups
|
||||||
|
if path.isfile(docBack):
|
||||||
|
unlink(docBack)
|
||||||
|
if path.isfile(docPath):
|
||||||
|
rename(docPath, docBack)
|
||||||
|
rename(docTemp, docPath)
|
||||||
|
|
||||||
self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName)
|
self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName)
|
||||||
|
|
||||||
@@ -132,8 +130,8 @@ class NWDoc():
|
|||||||
dataPath = path.join(self.theProject.projPath, docDir)
|
dataPath = path.join(self.theProject.projPath, docDir)
|
||||||
chkList = []
|
chkList = []
|
||||||
chkList.append(path.join(dataPath, docFile))
|
chkList.append(path.join(dataPath, docFile))
|
||||||
chkList.append(path.join(dataPath,docFile[:-3]+"tmp"))
|
chkList.append(path.join(dataPath, docFile+"~"))
|
||||||
chkList.append(path.join(dataPath,docFile[:-3]+"bak"))
|
chkList.append(path.join(dataPath, docFile[:-3]+"bak"))
|
||||||
for chkFile in chkList:
|
for chkFile in chkList:
|
||||||
if path.isfile(chkFile):
|
if path.isfile(chkFile):
|
||||||
try:
|
try:
|
||||||
@@ -147,7 +145,7 @@ class NWDoc():
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def assemblePath(tHandle, docExt):
|
def assemblePath(tHandle, docExt):
|
||||||
if tHandle is None:
|
if tHandle is None:
|
||||||
return None
|
return None, None
|
||||||
docDir = "data_"+tHandle[0]
|
docDir = "data_"+tHandle[0]
|
||||||
docFile = tHandle[1:13]+"_"+docExt
|
docFile = tHandle[1:13]+"_"+docExt
|
||||||
return docDir, docFile
|
return docDir, docFile
|
||||||
|
|||||||
+49
-60
@@ -13,7 +13,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import nw
|
import nw
|
||||||
|
|
||||||
from os import path, mkdir, listdir, unlink
|
from os import path, mkdir, listdir, unlink, rename
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
@@ -22,6 +22,7 @@ from time import time
|
|||||||
|
|
||||||
from nw.project.status import NWStatus
|
from nw.project.status import NWStatus
|
||||||
from nw.project.item import NWItem
|
from nw.project.item import NWItem
|
||||||
|
from nw.tools import projectMaintenance
|
||||||
from nw.common import checkString, checkBool, checkInt
|
from nw.common import checkString, checkBool, checkInt
|
||||||
from nw.constants import (
|
from nw.constants import (
|
||||||
nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||||
@@ -50,7 +51,6 @@ class NWProject():
|
|||||||
self.trashRoot = None # The handle of the trash root folder
|
self.trashRoot = None # The handle of the trash root folder
|
||||||
self.projPath = None # The full path to where the currently open project is saved
|
self.projPath = None # The full path to where the currently open project is saved
|
||||||
self.projMeta = None # The full path to the project's meta data folder
|
self.projMeta = None # The full path to the project's meta data folder
|
||||||
self.projCache = None # The full path to the project's cache folder
|
|
||||||
self.projDict = None # The spell check dictionary
|
self.projDict = None # The spell check dictionary
|
||||||
self.projFile = None # The file name of the project main xml file
|
self.projFile = None # The file name of the project main xml file
|
||||||
|
|
||||||
@@ -154,7 +154,6 @@ class NWProject():
|
|||||||
self.trashRoot = None
|
self.trashRoot = None
|
||||||
self.projPath = None
|
self.projPath = None
|
||||||
self.projMeta = None
|
self.projMeta = None
|
||||||
self.projCache = None
|
|
||||||
self.projDict = None
|
self.projDict = None
|
||||||
self.projFile = nwFiles.PROJ_FILE
|
self.projFile = nwFiles.PROJ_FILE
|
||||||
self.projName = ""
|
self.projName = ""
|
||||||
@@ -180,6 +179,11 @@ class NWProject():
|
|||||||
return
|
return
|
||||||
|
|
||||||
def openProject(self, fileName):
|
def openProject(self, fileName):
|
||||||
|
"""Open the project file provided, or if 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 path.isfile(fileName):
|
if not path.isfile(fileName):
|
||||||
fileName = path.join(fileName, nwFiles.PROJ_FILE)
|
fileName = path.join(fileName, nwFiles.PROJ_FILE)
|
||||||
@@ -191,21 +195,35 @@ class NWProject():
|
|||||||
self.projPath = path.dirname(fileName)
|
self.projPath = path.dirname(fileName)
|
||||||
logger.debug("Opening project: %s" % self.projPath)
|
logger.debug("Opening project: %s" % self.projPath)
|
||||||
|
|
||||||
self.projMeta = path.join(self.projPath,"meta")
|
self.projMeta = path.join(self.projPath,"meta")
|
||||||
self.projCache = path.join(self.projPath,"cache")
|
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
|
||||||
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
|
|
||||||
|
|
||||||
if not self._checkFolder(self.projMeta):
|
if not self._checkFolder(self.projMeta):
|
||||||
return
|
return
|
||||||
if not self._checkFolder(self.projCache):
|
|
||||||
return
|
try:
|
||||||
|
projectMaintenance(self)
|
||||||
|
except Exception as E:
|
||||||
|
logger.error(str(E))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
nwXML = etree.parse(fileName)
|
nwXML = etree.parse(fileName)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR)
|
self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR)
|
||||||
self.clearProject()
|
|
||||||
return False
|
# Trying to open backup file instead
|
||||||
|
backFile = fileName[:-3]+"bak"
|
||||||
|
if path.isfile(backFile):
|
||||||
|
self.makeAlert("Attempting to open backup project file instead.", nwAlert.INFO)
|
||||||
|
try:
|
||||||
|
nwXML = etree.parse(backFile)
|
||||||
|
except Exception as e:
|
||||||
|
self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR)
|
||||||
|
self.clearProject()
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.clearProject()
|
||||||
|
return False
|
||||||
|
|
||||||
xRoot = nwXML.getroot()
|
xRoot = nwXML.getroot()
|
||||||
nwxRoot = xRoot.tag
|
nwxRoot = xRoot.tag
|
||||||
@@ -288,25 +306,24 @@ class NWProject():
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def saveProject(self, isAuto=False):
|
def saveProject(self):
|
||||||
|
"""Save the project main XML file. The saving command itself
|
||||||
|
uses a temporary filename, and the file is renamed afterwards to
|
||||||
|
make sure if the save fails, we're not left with a truncated
|
||||||
|
file.
|
||||||
|
"""
|
||||||
|
|
||||||
if self.projPath is None:
|
if self.projPath is None:
|
||||||
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
|
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.projMeta = path.join(self.projPath,"meta")
|
self.projMeta = path.join(self.projPath,"meta")
|
||||||
self.projCache = path.join(self.projPath,"cache")
|
|
||||||
|
|
||||||
if not self._checkFolder(self.projPath): return
|
if not self._checkFolder(self.projPath): return
|
||||||
if not self._checkFolder(self.projMeta): return
|
if not self._checkFolder(self.projMeta): return
|
||||||
if not self._checkFolder(self.projCache): return
|
|
||||||
|
|
||||||
logger.debug("Saving project: %s" % self.projPath)
|
logger.debug("Saving project: %s" % self.projPath)
|
||||||
|
|
||||||
# Save a copy of the current file, just in case
|
|
||||||
if not isAuto:
|
|
||||||
self._maintainPrevious()
|
|
||||||
|
|
||||||
# Root element and project details
|
# Root element and project details
|
||||||
logger.debug("Writing project meta")
|
logger.debug("Writing project meta")
|
||||||
nwXML = etree.Element("novelWriterXML",attrib={
|
nwXML = etree.Element("novelWriterXML",attrib={
|
||||||
@@ -345,9 +362,11 @@ class NWProject():
|
|||||||
self.projTree[tHandle].packXML(xContent)
|
self.projTree[tHandle].packXML(xContent)
|
||||||
|
|
||||||
# Write the xml tree to file
|
# Write the xml tree to file
|
||||||
saveFile = path.join(self.projPath,self.projFile)
|
tempFile = path.join(self.projPath, self.projFile+"~")
|
||||||
|
saveFile = path.join(self.projPath, self.projFile)
|
||||||
|
backFile = path.join(self.projPath, self.projFile[:-3]+"bak")
|
||||||
try:
|
try:
|
||||||
with open(saveFile,mode="wb") as outFile:
|
with open(tempFile, mode="wb") as outFile:
|
||||||
outFile.write(etree.tostring(
|
outFile.write(etree.tostring(
|
||||||
nwXML,
|
nwXML,
|
||||||
pretty_print = True,
|
pretty_print = True,
|
||||||
@@ -358,6 +377,14 @@ class NWProject():
|
|||||||
self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR)
|
self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# If we're here, the file was successfully saved,
|
||||||
|
# so let's sort out the temps and backups
|
||||||
|
if path.isfile(backFile):
|
||||||
|
unlink(backFile)
|
||||||
|
if path.isfile(saveFile):
|
||||||
|
rename(saveFile, backFile)
|
||||||
|
rename(tempFile, saveFile)
|
||||||
|
|
||||||
self.mainConf.setRecent(self.projPath)
|
self.mainConf.setRecent(self.projPath)
|
||||||
self.theParent.setStatus("Saved Project: %s" % self.projName)
|
self.theParent.setStatus("Saved Project: %s" % self.projName)
|
||||||
self.setProjectChanged(False)
|
self.setProjectChanged(False)
|
||||||
@@ -740,42 +767,4 @@ class NWProject():
|
|||||||
itemHandle = self._makeHandle(addSeed+"!")
|
itemHandle = self._makeHandle(addSeed+"!")
|
||||||
return itemHandle
|
return itemHandle
|
||||||
|
|
||||||
def _maintainPrevious(self):
|
|
||||||
"""This function will take the current project file and copy it
|
|
||||||
into the project cache folder with an incremental file extension
|
|
||||||
added. These serve as a backup in case the xml file gets
|
|
||||||
corrupted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
countFile = path.join(self.projCache, nwFiles.PROJ_COUNT)
|
|
||||||
projCount = 0
|
|
||||||
|
|
||||||
if path.isfile(countFile):
|
|
||||||
try:
|
|
||||||
with open(countFile, mode="r") as inFile:
|
|
||||||
projCount = int(inFile.read())+1
|
|
||||||
except:
|
|
||||||
projCount = 0
|
|
||||||
|
|
||||||
if projCount > 9:
|
|
||||||
projCount = 0
|
|
||||||
|
|
||||||
projBackup = "%s.%d" % (nwFiles.PROJ_FILE, projCount)
|
|
||||||
|
|
||||||
try:
|
|
||||||
copyfile(
|
|
||||||
path.join(self.projPath, self.projFile),
|
|
||||||
path.join(self.projCache, projBackup)
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
logger.error("Failed to write to file %s" % projBackup)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(countFile, mode="w") as outFile:
|
|
||||||
outFile.write(str(projCount))
|
|
||||||
except:
|
|
||||||
logger.error("Failed to write to file %s" % countFile)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
# END Class NWProject
|
# END Class NWProject
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from nw.tools.analyse import TextAnalysis
|
from nw.tools.analyse import TextAnalysis
|
||||||
|
from nw.tools.legacy import projectMaintenance
|
||||||
from nw.tools.optlaststate import OptLastState
|
from nw.tools.optlaststate import OptLastState
|
||||||
from nw.tools.spellcheck import NWSpellCheck
|
from nw.tools.spellcheck import NWSpellCheck
|
||||||
from nw.tools.spellenchant import NWSpellEnchant
|
from nw.tools.spellenchant import NWSpellEnchant
|
||||||
@@ -10,6 +11,7 @@ from nw.tools.wordcount import countWords
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"TextAnalysis",
|
"TextAnalysis",
|
||||||
|
"projectMaintenance",
|
||||||
"OptLastState",
|
"OptLastState",
|
||||||
"NWSpellCheck",
|
"NWSpellCheck",
|
||||||
"NWSpellEnchant",
|
"NWSpellEnchant",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Legacy Tools
|
||||||
|
|
||||||
|
novelWriter – Legacy Tools
|
||||||
|
============================
|
||||||
|
Various functions to handle old projects
|
||||||
|
|
||||||
|
File History:
|
||||||
|
Created: 2020-02-13 [0.4.3]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import nw
|
||||||
|
|
||||||
|
from os import path, unlink, rmdir
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def projectMaintenance(theProject):
|
||||||
|
"""Wrapper class for handling various tasks related to managing old
|
||||||
|
projects with content from older versions of novelWriter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Remove no longer used project cache folder
|
||||||
|
if path.isdir(theProject.projPath):
|
||||||
|
cacheDir = path.join(theProject.projPath, "cache")
|
||||||
|
if path.isdir(cacheDir):
|
||||||
|
logger.info("Deprecated cache folder found")
|
||||||
|
rmList = []
|
||||||
|
for i in range(10):
|
||||||
|
rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i))
|
||||||
|
rmList.append(path.join(cacheDir, "projCount.txt"))
|
||||||
|
for rmFile in rmList:
|
||||||
|
if path.isfile(rmFile):
|
||||||
|
logger.info("Deleting: %s" % rmFile)
|
||||||
|
try:
|
||||||
|
unlink(rmFile)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
logger.info("Deleting: %s" % cacheDir)
|
||||||
|
try:
|
||||||
|
rmdir(cacheDir)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
|
||||||
|
return
|
||||||
@@ -37,7 +37,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
|
|||||||
assert nwGUI.theProject.trashRoot is None
|
assert nwGUI.theProject.trashRoot is None
|
||||||
assert nwGUI.theProject.projPath is None
|
assert nwGUI.theProject.projPath is None
|
||||||
assert nwGUI.theProject.projMeta is None
|
assert nwGUI.theProject.projMeta is None
|
||||||
assert nwGUI.theProject.projCache is None
|
|
||||||
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
||||||
assert nwGUI.theProject.projName == ""
|
assert nwGUI.theProject.projName == ""
|
||||||
assert nwGUI.theProject.bookTitle == ""
|
assert nwGUI.theProject.bookTitle == ""
|
||||||
@@ -62,7 +61,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
|
|||||||
assert nwGUI.theProject.trashRoot is None
|
assert nwGUI.theProject.trashRoot is None
|
||||||
assert nwGUI.theProject.projPath == nwTempGUI
|
assert nwGUI.theProject.projPath == nwTempGUI
|
||||||
assert nwGUI.theProject.projMeta == path.join(nwTempGUI,"meta")
|
assert nwGUI.theProject.projMeta == path.join(nwTempGUI,"meta")
|
||||||
assert nwGUI.theProject.projCache == path.join(nwTempGUI,"cache")
|
|
||||||
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
||||||
assert nwGUI.theProject.projName == ""
|
assert nwGUI.theProject.projName == ""
|
||||||
assert nwGUI.theProject.bookTitle == ""
|
assert nwGUI.theProject.bookTitle == ""
|
||||||
|
|||||||
Reference in New Issue
Block a user