MrDraw/src/octoprint/server/api/settings.py
Gina Häußge bccc706329 First throw at caching of API methods
Most caching is left to the client, by utilizing ETag and Last-Modified headers.

Where it was easily achievable, an additional server side miniature cache of intermediary
results was introduced (e.g. for the files). The regular cached decorator was not used
since it targets caching full responses, and the responses in question already contained
client request specific data. Caching "one step earlier" allows better usage of the cache here.

Also introduced a dependency on the scandir module, to get a bit of a performance boost
on os.walk and os.listdir (which have been replaced with scandir.walk and scandir.listdir
respectively). See https://github.com/benhoyt/scandir#background on why that made
sense.
2016-08-30 19:02:30 +02:00

350 lines
20 KiB
Python

# coding=utf-8
from __future__ import absolute_import, division, print_function
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
import logging
from flask import request, jsonify, make_response
from werkzeug.exceptions import BadRequest
from octoprint.events import eventManager, Events
from octoprint.settings import settings, valid_boolean_trues
from octoprint.server import admin_permission, printer
from octoprint.server.api import api
from octoprint.server.util.flask import restricted_access, with_revalidation_checking
import octoprint.plugin
import octoprint.util
#~~ settings
def _lastmodified():
return settings().last_modified
def _etag(lm=None):
if lm is None:
lm = _lastmodified()
connection_options = printer.__class__.get_connection_options()
import hashlib
hash = hashlib.sha1()
hash.update(str(lm))
hash.update(repr(connection_options))
return hash.hexdigest()
@api.route("/settings", methods=["GET"])
@with_revalidation_checking(etag_factory=_etag,
lastmodified_factory=_lastmodified,
unless=lambda: request.values.get("force", "false") in valid_boolean_trues)
def getSettings():
logger = logging.getLogger(__name__)
s = settings()
connectionOptions = printer.__class__.get_connection_options()
data = {
"api": {
"enabled": s.getBoolean(["api", "enabled"]),
"key": s.get(["api", "key"]) if admin_permission.can() else "n/a",
"allowCrossOrigin": s.get(["api", "allowCrossOrigin"])
},
"appearance": {
"name": s.get(["appearance", "name"]),
"color": s.get(["appearance", "color"]),
"colorTransparent": s.getBoolean(["appearance", "colorTransparent"]),
"defaultLanguage": s.get(["appearance", "defaultLanguage"]),
"showFahrenheitAlso": s.getBoolean(["appearance", "showFahrenheitAlso"])
},
"printer": {
"defaultExtrusionLength": s.getInt(["printerParameters", "defaultExtrusionLength"])
},
"webcam": {
"streamUrl": s.get(["webcam", "stream"]),
"snapshotUrl": s.get(["webcam", "snapshot"]),
"ffmpegPath": s.get(["webcam", "ffmpeg"]),
"bitrate": s.get(["webcam", "bitrate"]),
"ffmpegThreads": s.get(["webcam", "ffmpegThreads"]),
"watermark": s.getBoolean(["webcam", "watermark"]),
"flipH": s.getBoolean(["webcam", "flipH"]),
"flipV": s.getBoolean(["webcam", "flipV"]),
"rotate90": s.getBoolean(["webcam", "rotate90"])
},
"feature": {
"gcodeViewer": s.getBoolean(["gcodeViewer", "enabled"]),
"sizeThreshold": s.getInt(["gcodeViewer", "sizeThreshold"]),
"mobileSizeThreshold": s.getInt(["gcodeViewer", "mobileSizeThreshold"]),
"temperatureGraph": s.getBoolean(["feature", "temperatureGraph"]),
"waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]),
"alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]),
"neverSendChecksum": s.getBoolean(["feature", "neverSendChecksum"]),
"sdSupport": s.getBoolean(["feature", "sdSupport"]),
"sdRelativePath": s.getBoolean(["feature", "sdRelativePath"]),
"sdAlwaysAvailable": s.getBoolean(["feature", "sdAlwaysAvailable"]),
"swallowOkAfterResend": s.getBoolean(["feature", "swallowOkAfterResend"]),
"repetierTargetTemp": s.getBoolean(["feature", "repetierTargetTemp"]),
"externalHeatupDetection": s.getBoolean(["feature", "externalHeatupDetection"]),
"keyboardControl": s.getBoolean(["feature", "keyboardControl"]),
"pollWatched": s.getBoolean(["feature", "pollWatched"]),
"ignoreIdenticalResends": s.getBoolean(["feature", "ignoreIdenticalResends"]),
"modelSizeDetection": s.getBoolean(["feature", "modelSizeDetection"])
},
"serial": {
"port": connectionOptions["portPreference"],
"baudrate": connectionOptions["baudratePreference"],
"portOptions": connectionOptions["ports"],
"baudrateOptions": connectionOptions["baudrates"],
"autoconnect": s.getBoolean(["serial", "autoconnect"]),
"timeoutConnection": s.getFloat(["serial", "timeout", "connection"]),
"timeoutDetection": s.getFloat(["serial", "timeout", "detection"]),
"timeoutCommunication": s.getFloat(["serial", "timeout", "communication"]),
"timeoutTemperature": s.getFloat(["serial", "timeout", "temperature"]),
"timeoutTemperatureTargetSet": s.getFloat(["serial", "timeout", "temperatureTargetSet"]),
"timeoutSdStatus": s.getFloat(["serial", "timeout", "sdStatus"]),
"log": s.getBoolean(["serial", "log"]),
"additionalPorts": s.get(["serial", "additionalPorts"]),
"additionalBaudrates": s.get(["serial", "additionalBaudrates"]),
"longRunningCommands": s.get(["serial", "longRunningCommands"]),
"checksumRequiringCommands": s.get(["serial", "checksumRequiringCommands"]),
"helloCommand": s.get(["serial", "helloCommand"]),
"ignoreErrorsFromFirmware": s.getBoolean(["serial", "ignoreErrorsFromFirmware"]),
"disconnectOnErrors": s.getBoolean(["serial", "disconnectOnErrors"]),
"triggerOkForM29": s.getBoolean(["serial", "triggerOkForM29"]),
"supportResendsWithoutOk": s.getBoolean(["serial", "supportResendsWithoutOk"]),
"maxTimeoutsIdle": s.getInt(["serial", "maxCommunicationTimeouts", "idle"]),
"maxTimeoutsPrinting": s.getInt(["serial", "maxCommunicationTimeouts", "printing"]),
"maxTimeoutsLong": s.getInt(["serial", "maxCommunicationTimeouts", "long"])
},
"folder": {
"uploads": s.getBaseFolder("uploads"),
"timelapse": s.getBaseFolder("timelapse"),
"timelapseTmp": s.getBaseFolder("timelapse_tmp"),
"logs": s.getBaseFolder("logs"),
"watched": s.getBaseFolder("watched")
},
"temperature": {
"profiles": s.get(["temperature", "profiles"]),
"cutoff": s.getInt(["temperature", "cutoff"])
},
"system": {
"actions": s.get(["system", "actions"]),
"events": s.get(["system", "events"])
},
"terminalFilters": s.get(["terminalFilters"]),
"scripts": {
"gcode": {
"afterPrinterConnected": None,
"beforePrinterDisconnected": None,
"beforePrintStarted": None,
"afterPrintCancelled": None,
"afterPrintDone": None,
"beforePrintPaused": None,
"afterPrintResumed": None,
"snippets": dict()
}
},
"server": {
"commands": {
"systemShutdownCommand": s.get(["server", "commands", "systemShutdownCommand"]),
"systemRestartCommand": s.get(["server", "commands", "systemRestartCommand"]),
"serverRestartCommand": s.get(["server", "commands", "serverRestartCommand"])
},
"diskspace": {
"warning": s.getInt(["server", "diskspace", "warning"]),
"critical": s.getInt(["server", "diskspace", "critical"])
}
}
}
gcode_scripts = s.listScripts("gcode")
if gcode_scripts:
data["scripts"] = dict(gcode=dict())
for name in gcode_scripts:
data["scripts"]["gcode"][name] = s.loadScript("gcode", name, source=True)
def process_plugin_result(name, result):
if result:
try:
jsonify(test=result)
except:
logger.exception("Error while jsonifying settings from plugin {}, please contact the plugin author about this".format(name))
if not "plugins" in data:
data["plugins"] = dict()
if "__enabled" in result:
del result["__enabled"]
data["plugins"][name] = result
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
try:
result = plugin.on_settings_load()
process_plugin_result(plugin._identifier, result)
except TypeError:
logger.warn("Could not load settings for plugin {name} ({version}) since it called super(...)".format(name=plugin._plugin_name, version=plugin._plugin_version))
logger.warn("in a way which has issues due to OctoPrint's dynamic reloading after plugin operations.")
logger.warn("Please contact the plugin's author and ask to update the plugin to use a direct call like")
logger.warn("octoprint.plugin.SettingsPlugin.on_settings_load(self) instead.")
except:
logger.exception("Could not load settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name))
return jsonify(data)
@api.route("/settings", methods=["POST"])
@restricted_access
@admin_permission.require(403)
def setSettings():
if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content-type JSON", 400)
try:
data = request.json
except BadRequest:
return make_response("Malformed JSON body in request", 400)
_saveSettings(data)
return getSettings()
def _saveSettings(data):
logger = logging.getLogger(__name__)
s = settings()
if "api" in data.keys():
if "enabled" in data["api"].keys(): s.setBoolean(["api", "enabled"], data["api"]["enabled"])
if "key" in data["api"].keys(): s.set(["api", "key"], data["api"]["key"], True)
if "allowCrossOrigin" in data["api"].keys(): s.setBoolean(["api", "allowCrossOrigin"], data["api"]["allowCrossOrigin"])
if "appearance" in data.keys():
if "name" in data["appearance"].keys(): s.set(["appearance", "name"], data["appearance"]["name"])
if "color" in data["appearance"].keys(): s.set(["appearance", "color"], data["appearance"]["color"])
if "colorTransparent" in data["appearance"].keys(): s.setBoolean(["appearance", "colorTransparent"], data["appearance"]["colorTransparent"])
if "defaultLanguage" in data["appearance"]: s.set(["appearance", "defaultLanguage"], data["appearance"]["defaultLanguage"])
if "showFahrenheitAlso" in data["appearance"]: s.setBoolean(["appearance", "showFahrenheitAlso"], data["appearance"]["showFahrenheitAlso"])
if "printer" in data.keys():
if "defaultExtrusionLength" in data["printer"]: s.setInt(["printerParameters", "defaultExtrusionLength"], data["printer"]["defaultExtrusionLength"])
if "webcam" in data.keys():
if "streamUrl" in data["webcam"].keys(): s.set(["webcam", "stream"], data["webcam"]["streamUrl"])
if "snapshotUrl" in data["webcam"].keys(): s.set(["webcam", "snapshot"], data["webcam"]["snapshotUrl"])
if "ffmpegPath" in data["webcam"].keys(): s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpegPath"])
if "bitrate" in data["webcam"].keys(): s.set(["webcam", "bitrate"], data["webcam"]["bitrate"])
if "ffmpegThreads" in data["webcam"].keys(): s.setInt(["webcam", "ffmpegThreads"], data["webcam"]["ffmpegThreads"])
if "watermark" in data["webcam"].keys(): s.setBoolean(["webcam", "watermark"], data["webcam"]["watermark"])
if "flipH" in data["webcam"].keys(): s.setBoolean(["webcam", "flipH"], data["webcam"]["flipH"])
if "flipV" in data["webcam"].keys(): s.setBoolean(["webcam", "flipV"], data["webcam"]["flipV"])
if "rotate90" in data["webcam"].keys(): s.setBoolean(["webcam", "rotate90"], data["webcam"]["rotate90"])
if "feature" in data.keys():
if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["gcodeViewer", "enabled"], data["feature"]["gcodeViewer"])
if "sizeThreshold" in data["feature"].keys(): s.setInt(["gcodeViewer", "sizeThreshold"], data["feature"]["sizeThreshold"])
if "mobileSizeThreshold" in data["feature"].keys(): s.setInt(["gcodeViewer", "mobileSizeThreshold"], data["feature"]["mobileSizeThreshold"])
if "temperatureGraph" in data["feature"].keys(): s.setBoolean(["feature", "temperatureGraph"], data["feature"]["temperatureGraph"])
if "waitForStart" in data["feature"].keys(): s.setBoolean(["feature", "waitForStartOnConnect"], data["feature"]["waitForStart"])
if "alwaysSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "alwaysSendChecksum"], data["feature"]["alwaysSendChecksum"])
if "neverSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "neverSendChecksum"], data["feature"]["neverSendChecksum"])
if "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"])
if "sdRelativePath" in data["feature"].keys(): s.setBoolean(["feature", "sdRelativePath"], data["feature"]["sdRelativePath"])
if "sdAlwaysAvailable" in data["feature"].keys(): s.setBoolean(["feature", "sdAlwaysAvailable"], data["feature"]["sdAlwaysAvailable"])
if "swallowOkAfterResend" in data["feature"].keys(): s.setBoolean(["feature", "swallowOkAfterResend"], data["feature"]["swallowOkAfterResend"])
if "repetierTargetTemp" in data["feature"].keys(): s.setBoolean(["feature", "repetierTargetTemp"], data["feature"]["repetierTargetTemp"])
if "externalHeatupDetection" in data["feature"].keys(): s.setBoolean(["feature", "externalHeatupDetection"], data["feature"]["externalHeatupDetection"])
if "keyboardControl" in data["feature"].keys(): s.setBoolean(["feature", "keyboardControl"], data["feature"]["keyboardControl"])
if "pollWatched" in data["feature"]: s.setBoolean(["feature", "pollWatched"], data["feature"]["pollWatched"])
if "ignoreIdenticalResends" in data["feature"]: s.setBoolean(["feature", "ignoreIdenticalResends"], data["feature"]["ignoreIdenticalResends"])
if "modelSizeDetection" in data["feature"]: s.setBoolean(["feature", "modelSizeDetection"], data["feature"]["modelSizeDetection"])
if "serial" in data.keys():
if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"])
if "port" in data["serial"].keys(): s.set(["serial", "port"], data["serial"]["port"])
if "baudrate" in data["serial"].keys(): s.setInt(["serial", "baudrate"], data["serial"]["baudrate"])
if "timeoutConnection" in data["serial"].keys(): s.setFloat(["serial", "timeout", "connection"], data["serial"]["timeoutConnection"])
if "timeoutDetection" in data["serial"].keys(): s.setFloat(["serial", "timeout", "detection"], data["serial"]["timeoutDetection"])
if "timeoutCommunication" in data["serial"].keys(): s.setFloat(["serial", "timeout", "communication"], data["serial"]["timeoutCommunication"])
if "timeoutTemperature" in data["serial"].keys(): s.setFloat(["serial", "timeout", "temperature"], data["serial"]["timeoutTemperature"])
if "timeoutTemperatureTargetSet" in data["serial"].keys(): s.setFloat(["serial", "timeout", "temperatureTargetSet"], data["serial"]["timeoutTemperatureTargetSet"])
if "timeoutSdStatus" in data["serial"].keys(): s.setFloat(["serial", "timeout", "sdStatus"], data["serial"]["timeoutSdStatus"])
if "additionalPorts" in data["serial"] and isinstance(data["serial"]["additionalPorts"], (list, tuple)): s.set(["serial", "additionalPorts"], data["serial"]["additionalPorts"])
if "additionalBaudrates" in data["serial"] and isinstance(data["serial"]["additionalBaudrates"], (list, tuple)): s.set(["serial", "additionalBaudrates"], data["serial"]["additionalBaudrates"])
if "longRunningCommands" in data["serial"] and isinstance(data["serial"]["longRunningCommands"], (list, tuple)): s.set(["serial", "longRunningCommands"], data["serial"]["longRunningCommands"])
if "checksumRequiringCommands" in data["serial"] and isinstance(data["serial"]["checksumRequiringCommands"], (list, tuple)): s.set(["serial", "checksumRequiringCommands"], data["serial"]["checksumRequiringCommands"])
if "helloCommand" in data["serial"]: s.set(["serial", "helloCommand"], data["serial"]["helloCommand"])
if "ignoreErrorsFromFirmware" in data["serial"]: s.setBoolean(["serial", "ignoreErrorsFromFirmware"], data["serial"]["ignoreErrorsFromFirmware"])
if "disconnectOnErrors" in data["serial"]: s.setBoolean(["serial", "disconnectOnErrors"], data["serial"]["disconnectOnErrors"])
if "triggerOkForM29" in data["serial"]: s.setBoolean(["serial", "triggerOkForM29"], data["serial"]["triggerOkForM29"])
if "supportResendsWithoutOk" in data["serial"]: s.setBoolean(["serial", "supportResendsWithoutOk"], data["serial"]["supportResendsWithoutOk"])
if "maxTimeoutsIdle" in data["serial"]: s.setInt(["serial", "maxCommunicationTimeouts", "idle"], data["serial"]["maxTimeoutsIdle"])
if "maxTimeoutsPrinting" in data["serial"]: s.setInt(["serial", "maxCommunicationTimeouts", "printing"], data["serial"]["maxTimeoutsPrinting"])
if "maxTimeoutsLong" in data["serial"]: s.setInt(["serial", "maxCommunicationTimeouts", "long"], data["serial"]["maxTimeoutsLong"])
oldLog = s.getBoolean(["serial", "log"])
if "log" in data["serial"].keys(): s.setBoolean(["serial", "log"], data["serial"]["log"])
if oldLog and not s.getBoolean(["serial", "log"]):
# disable debug logging to serial.log
logging.getLogger("SERIAL").debug("Disabling serial logging")
logging.getLogger("SERIAL").setLevel(logging.CRITICAL)
elif not oldLog and s.getBoolean(["serial", "log"]):
# enable debug logging to serial.log
logging.getLogger("SERIAL").setLevel(logging.DEBUG)
logging.getLogger("SERIAL").debug("Enabling serial logging")
if "folder" in data.keys():
if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"])
if "timelapse" in data["folder"].keys(): s.setBaseFolder("timelapse", data["folder"]["timelapse"])
if "timelapseTmp" in data["folder"].keys(): s.setBaseFolder("timelapse_tmp", data["folder"]["timelapseTmp"])
if "logs" in data["folder"].keys(): s.setBaseFolder("logs", data["folder"]["logs"])
if "watched" in data["folder"].keys(): s.setBaseFolder("watched", data["folder"]["watched"])
if "temperature" in data.keys():
if "profiles" in data["temperature"].keys(): s.set(["temperature", "profiles"], data["temperature"]["profiles"])
if "cutoff" in data["temperature"].keys(): s.setInt(["temperature", "cutoff"], data["temperature"]["cutoff"])
if "terminalFilters" in data.keys():
s.set(["terminalFilters"], data["terminalFilters"])
if "system" in data.keys():
if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"])
if "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"])
if "scripts" in data:
if "gcode" in data["scripts"] and isinstance(data["scripts"]["gcode"], dict):
for name, script in data["scripts"]["gcode"].items():
if name == "snippets":
continue
s.saveScript("gcode", name, script.replace("\r\n", "\n").replace("\r", "\n"))
if "server" in data:
if "commands" in data["server"]:
if "systemShutdownCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "systemShutdownCommand"], data["server"]["commands"]["systemShutdownCommand"])
if "systemRestartCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "systemRestartCommand"], data["server"]["commands"]["systemRestartCommand"])
if "serverRestartCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "serverRestartCommand"], data["server"]["commands"]["serverRestartCommand"])
if "diskspace" in data["server"]:
if "warning" in data["server"]["diskspace"]: s.setInt(["server", "diskspace", "warning"], data["server"]["diskspace"]["warning"])
if "critical" in data["server"]["diskspace"]: s.setInt(["server", "diskspace", "critical"], data["server"]["diskspace"]["critical"])
if "plugins" in data:
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
plugin_id = plugin._identifier
if plugin_id in data["plugins"]:
try:
plugin.on_settings_save(data["plugins"][plugin_id])
except TypeError:
logger.warn("Could not save settings for plugin {name} ({version}) since it called super(...)".format(name=plugin._plugin_name, version=plugin._plugin_version))
logger.warn("in a way which has issues due to OctoPrint's dynamic reloading after plugin operations.")
logger.warn("Please contact the plugin's author and ask to update the plugin to use a direct call like")
logger.warn("octoprint.plugin.SettingsPlugin.on_settings_save(self, data) instead.")
except:
logger.exception("Could not save settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name))
if s.save():
payload = dict(
config_hash=s.config_hash,
effective_hash=s.effective_hash
)
eventManager().fire(Events.SETTINGS_UPDATED, payload=payload)