Merge pull request #165 from vkbo/file_save_update

File saving and opening
This commit is contained in:
Veronica K. Berglyd Olsen
2020-02-13 21:27:26 +01:00
committed by GitHub
7 changed files with 114 additions and 81 deletions
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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",
+47
View File
@@ -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
-2
View File
@@ -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 == ""