Skip to content

Commit

Permalink
Merge pull request #17 from Webguyatwork/br-external-db-support
Browse files Browse the repository at this point in the history
External database support
  • Loading branch information
dojohnso authored Jan 23, 2024
2 parents 379bd39 + 863cf91 commit 23d9762
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 144 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Released](https://img.shields.io/badge/dynamic/json.svg?color=brightgreen&label=released&url=https://api.github.com/repos/dojohnso/OctoPrint-SpoolManager/releases&query=$[0].published_at)]()
![GitHub Releases (by Release)](https://img.shields.io/github/downloads/dojohnso/OctoPrint-SpoolManager/latest/total.svg)

The OctoPrint-Plugin manages all spool informations and stores it in a database.
The OctoPrint-Plugin manages all spool informations and stores it in a database. Now includes the option to store to an external Postgres or MySQL database to share across multiple instances of OctoPrint.

#### *NOTE: this plugin has been abandoned by the original creator and adopted here by a new maintainer*

Expand All @@ -13,7 +13,7 @@ The OctoPrint-Plugin manages all spool informations and stores it in a database.
<a href="https://www.buymeacoffee.com/djohnson.tech" target="_blank"><img src="https://djohnson.tech/images/white-button.png" width=300 /></a>

## Tested with:
- OctoPrint 1.7.2: with Python 3.7.3
- OctoPrint 1.9.3: with Python 3.11.5

## Included features

Expand All @@ -39,9 +39,9 @@ The OctoPrint-Plugin manages all spool informations and stores it in a database.
- [X] Scan QR/Barcodes of a spool
- [X] Multi Tool support
- [X] Support for manual mid-print filament change
- [X] External Database Support (MySQL or Postgres)

## Planning / next features
- [ ] External Database (IN PROGRESS)
- [ ] PrintJobHistory integration [PrintJobHistory-Plugin](https://github.com/dojohnso/OctoPrint-PrintJobHistory)
- [ ] Capture Spool-Image
- [ ] ...more planing details could be found [here](https://github.com/dojohnso/OctoPrint-SpoolManager/projects/1)
Expand All @@ -58,6 +58,8 @@ The OctoPrint-Plugin manages all spool informations and stores it in a database.

![scanSpool-dialog](screenshots/scanSpool-dialog.png "ScanSpool-Dialog")

![externaldb-dialog](screenshots/externalDatabase.png "External-Database")

## Setup
Install via the bundled [Plugin Manager](http://docs.octoprint.org/en/master/bundledplugins/pluginmanager.html)
or manually using this URL:
Expand Down
144 changes: 113 additions & 31 deletions octoprint_SpoolManager/DatabaseManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from octoprint_SpoolManager.WrappedLoggingHandler import WrappedLoggingHandler
from peewee import *
from playhouse.shortcuts import model_to_dict, dict_to_model

from octoprint_SpoolManager.api import Transformer
from octoprint_SpoolManager.common import StringUtils
Expand Down Expand Up @@ -74,7 +75,7 @@ def _buildDatabaseConnection(self):
databaseType = self._databaseSettings.type
databaseName = self._databaseSettings.name
host = self._databaseSettings.host
port = self._databaseSettings.port
port = int(self._databaseSettings.port)
user = self._databaseSettings.user
password = self._databaseSettings.password
if ("postgres" == databaseType):
Expand Down Expand Up @@ -111,8 +112,8 @@ def _createOrUpgradeSchemeIfNecessary(self):
schemeVersionFromDatabaseModel = None
schemeVersionFromDatabase = None
try:
cursor = self.db.execute_sql('select "value" from "spo_pluginmetadatamodel" where key="'+PluginMetaDataModel.KEY_DATABASE_SCHEME_VERSION+'";')
result = cursor.fetchone()
cursor = PluginMetaDataModel.get(PluginMetaDataModel.key == PluginMetaDataModel.KEY_DATABASE_SCHEME_VERSION)
result = cursor.value
if (result != None):
schemeVersionFromDatabase = int(result[0])
self._logger.info("Current databasescheme: " + str(schemeVersionFromDatabase))
Expand Down Expand Up @@ -602,6 +603,10 @@ def initDatabase(self, databaseSettings, sendMessageToClient):
existsDatabaseFile = str(os.path.exists(self._databaseSettings.fileLocation))
self._logger.info("Databasefile '" +self._databaseSettings.fileLocation+ "' exists: " + existsDatabaseFile)

if (existsDatabaseFile == False):
self._createDatabase(FORCE_CREATE_TABLES)
self.closeDatabase()

import logging
logger = logging.getLogger('peewee')
# we need only the single logger without parent
Expand Down Expand Up @@ -637,8 +642,8 @@ def testDatabaseConnection(self, databaseSettings = None):
backupCurrentDatabaseSettings = self._databaseSettings
self._databaseSettings = databaseSettings

succesfull = self.connectoToDatabase()
if (succesfull == False):
succesful = self.connectoToDatabase()
if (succesful == False):
result = self.getCurrentErrorMessageDict()
finally:
try:
Expand All @@ -663,7 +668,7 @@ def connectoToDatabase(self, withMetaCheck=False, sendErrorPopUp=True) :
# build connection
try:
if (self.sqlLoggingEnabled):
self._logger.info("Databaseconnection with...")
self._logger.info("Database connection with...")
self._logger.info(self._databaseSettings)
self._database = self._buildDatabaseConnection()

Expand All @@ -673,7 +678,7 @@ def connectoToDatabase(self, withMetaCheck=False, sendErrorPopUp=True) :

self._database.connect()
if (self.sqlLoggingEnabled):
self._logger.info("Database connection succesful. Checking Scheme versions")
self._logger.info("Database connection successful. Checking Scheme versions")
# TODO do I realy need to check the meta-infos in the connect function
# schemeVersionFromPlugin = str(CURRENT_DATABASE_SCHEME_VERSION)
# schemeVersionFromDatabaseModel = str(PluginMetaDataModel.get(PluginMetaDataModel.key == PluginMetaDataModel.KEY_DATABASE_SCHEME_VERSION).value)
Expand Down Expand Up @@ -720,34 +725,38 @@ def showSQLLogging(self, enabled):

def backupDatabaseFile(self):

if (os.path.exists(self._databaseSettings.fileLocation)):
self._logger.info("Starting database backup")
now = datetime.datetime.now()
currentDate = now.strftime("%Y%m%d-%H%M")
currentSchemeVersion = "unknown"
try:
currentSchemeVersion = PluginMetaDataModel.get(
PluginMetaDataModel.key == PluginMetaDataModel.KEY_DATABASE_SCHEME_VERSION)
if (currentSchemeVersion != None):
currentSchemeVersion = str(currentSchemeVersion.value)
except Exception as e:
self._logger.exception("Could not read databasescheme version:" + str(e))

backupDatabaseFilePath = self._databaseSettings.fileLocation[0:-3] + "-backup-V" + currentSchemeVersion + "-" +currentDate+".db"
# backupDatabaseFileName = "spoolmanager-backup-"+currentDate+".db"
# backupDatabaseFilePath = os.path.join(backupFolder, backupDatabaseFileName)
if not os.path.exists(backupDatabaseFilePath):
shutil.copy(self._databaseSettings.fileLocation, backupDatabaseFilePath)
self._logger.info("Backup of spoolmanager database created '" + backupDatabaseFilePath + "'")
else:
self._logger.warn("Backup of spoolmanager database ('" + backupDatabaseFilePath + "') is already present. No backup created.")
return backupDatabaseFilePath
if (self._databaseSettings.useExternal == True):
self._logger.info("No database backup needed, because we are using an external database.")
else:
self._logger.info("No database backup needed, because there is no databasefile '"+str(self._databaseSettings.fileLocation)+"'")
if (os.path.exists(self._databaseSettings.fileLocation)):
self._logger.info("Starting database backup")
now = datetime.datetime.now()
currentDate = now.strftime("%Y%m%d-%H%M")
currentSchemeVersion = "unknown"
try:
currentSchemeVersion = PluginMetaDataModel.get(
PluginMetaDataModel.key == PluginMetaDataModel.KEY_DATABASE_SCHEME_VERSION)
if (currentSchemeVersion != None):
currentSchemeVersion = str(currentSchemeVersion.value)
except Exception as e:
self._logger.exception("Could not read databasescheme version:" + str(e))

backupDatabaseFilePath = self._databaseSettings.fileLocation[0:-3] + "-backup-V" + currentSchemeVersion + "-" +currentDate+".db"
# backupDatabaseFileName = "spoolmanager-backup-"+currentDate+".db"
# backupDatabaseFilePath = os.path.join(backupFolder, backupDatabaseFileName)
if not os.path.exists(backupDatabaseFilePath):
shutil.copy(self._databaseSettings.fileLocation, backupDatabaseFilePath)
self._logger.info("Backup of spoolmanager database created '" + backupDatabaseFilePath + "'")
else:
self._logger.warn("Backup of spoolmanager database ('" + backupDatabaseFilePath + "') is already present. No backup created.")
return backupDatabaseFilePath
else:
self._logger.info("No database backup needed, because there is no databasefile '"+str(self._databaseSettings.fileLocation)+"'")

def reCreateDatabase(self, databaseSettings = None):
self._currentErrorMessageDict = None
self._logger.info("ReCreating Database")
self._logger.info(databaseSettings)

backupCurrentDatabaseSettings = None
if (databaseSettings != None):
Expand All @@ -766,6 +775,74 @@ def reCreateDatabase(self, databaseSettings = None):
if (backupCurrentDatabaseSettings != None):
self._databaseSettings = backupCurrentDatabaseSettings

def copySpoolData(self, databaseSettings = None):

loadResult = False
copySpoolCount = 0

backupCurrentDatabaseSettings = None
if (databaseSettings != None):
backupCurrentDatabaseSettings = self._databaseSettings
else:
# use default settings
databaseSettings = self._databaseSettings
backupCurrentDatabaseSettings = self._databaseSettings

try:
currentDatabaseType = databaseSettings.type
currentUseExternal = databaseSettings.useExternal

# First load meta from local sqlite database
databaseSettings.type = "sqlite"
databaseSettings.baseFolder = self._databaseSettings.baseFolder
databaseSettings.fileLocation = self._databaseSettings.fileLocation
databaseSettings.useExternal = False
self._databaseSettings = databaseSettings

try:
self.connectoToDatabase( sendErrorPopUp=False)
allSpools = SpoolModel.select()
self.closeDatabase()
except Exception as e:
errorMessage = "local database: " + str(e)
self._logger.error("Connecting to local database not possible")
self._logger.exception(e)
try:
self.closeDatabase()
except Exception:
pass # ignore close exception


databaseSettings.type = currentDatabaseType
databaseSettings.useExternal = True
self._databaseSettings = databaseSettings

try:
self.connectoToDatabase( sendErrorPopUp=False)
self._createDatabase(True)
for spool in allSpools:
spoolJson = model_to_dict(spool)
SpoolModel.insert(spoolJson).execute()
copySpoolCount = copySpoolCount + 1
self.closeDatabase()
except Exception as e:
errorMessage = "database: " + str(e)
self._logger.error("Connecting to external database not possible")
self._logger.exception(e)
try:
self.closeDatabase()
except Exception:
pass # ignore close exception

finally:
# restore orig. databasettings
if (backupCurrentDatabaseSettings != None):
self._databaseSettings = backupCurrentDatabaseSettings

return {
"success": loadResult,
"copySpoolCount": copySpoolCount
}

################################################################################################ DATABASE OPERATIONS
def _handleReusableConnection(self, databaseCallMethode, withReusedConnection, methodeNameForLogging, defaultReturnValue=None):
Expand Down Expand Up @@ -820,11 +897,13 @@ def loadDatabaseMetaInformations(self, databaseSettings = None):
# always read local meta data
try:
currentDatabaseType = databaseSettings.type
currentUseExternal = databaseSettings.useExternal

# First load meta from local sqlite database
databaseSettings.type = "sqlite"
databaseSettings.baseFolder = self._databaseSettings.baseFolder
databaseSettings.fileLocation = self._databaseSettings.fileLocation
databaseSettings.useExternal = False
self._databaseSettings = databaseSettings
try:
self.connectoToDatabase( sendErrorPopUp=False)
Expand All @@ -840,8 +919,9 @@ def loadDatabaseMetaInformations(self, databaseSettings = None):
except Exception:
pass # ignore close exception

# Use orign Databasetype to collect the other meta dtaa (if neeeded)
# Use orign Databasetype to collect the other meta data (if neeeded)
databaseSettings.type = currentDatabaseType
databaseSettings.useExternal = currentUseExternal
if (databaseSettings.useExternal == True):
# External DB
self._databaseSettings = databaseSettings
Expand Down Expand Up @@ -998,6 +1078,8 @@ def databaseCallMethode():
myQuery = myQuery.order_by(SpoolModel.material.desc())
else:
myQuery = myQuery.order_by(SpoolModel.material.asc())

self._logger.info("Quering spools: %s" % myQuery)
return myQuery

return self._handleReusableConnection(databaseCallMethode, withReusedConnection, "loadAllSpoolsByQuery")
Expand Down
22 changes: 13 additions & 9 deletions octoprint_SpoolManager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,14 +743,17 @@ def on_settings_save(self, data):
if (selectedSpool != None):
self.set_temp_offsets(toolIndex, selectedSpool)

#
# databaseSettings = self._buildDatabaseSettingsFromPluginSettings()
#
# self._databaseManager.assignNewDatabaseSettings(databaseSettings)
# testResult = self._databaseManager.testDatabaseConnection(databaseSettings)
# if (testResult != None):
# # TODO Send to client
# pass
# In case we are switching between internal and external storage
databaseSettings = self._buildDatabaseSettingsFromPluginSettings()
self._databaseManager.assignNewDatabaseSettings(databaseSettings)
# testResult = self._databaseManager.testDatabaseConnection(databaseSettings)
# if (testResult != None):
# # TODO Send to client
# pass

self._sendDataToClient(dict(
action="reloadTable and sidebarSpools"
))


# to allow the frontend to trigger an update
Expand Down Expand Up @@ -925,7 +928,8 @@ def register_custom_events(*args, **kwargs):
EventBusKeys.EVENT_BUS_SPOOL_DELETED
]


def is_blueprint_csrf_protected(self):
return True

# def message_on_connect(self, comm, script_type, script_name, *args, **kwargs):
# print(script_name)
Expand Down
Loading

0 comments on commit 23d9762

Please sign in to comment.