Added user apikeys, made ajax api available via apikeys, restified ajax api (WIP)

This commit is contained in:
Gina Häußge 2013-11-19 22:53:26 +01:00
parent ae3e474c92
commit e690053fd4
21 changed files with 730 additions and 311 deletions

View file

@ -165,9 +165,15 @@ class GcodeManager:
#~~ file handling
def addFile(self, file, destination):
from octoprint.filemanager.destinations import FileDestinations
def addFile(self, file, destination, uploadCallback=None):
"""
Adds the given file for the given destination to the systems. Takes care of slicing if enabled and
necessary.
If the file's processing won't be finished directly with the return from this method but happen
asynchronously in the background (e.g. due to slicing), returns a tuple containing the just added file's
filename and False. Otherwise returns a tuple (filename, True).
"""
if not file or not destination:
return None, True
@ -183,14 +189,12 @@ class GcodeManager:
file.save(absolutePath)
if gcode:
return self.processGcode(absolutePath), True
return self.processGcode(absolutePath, destination, uploadCallback), True
else:
local = (destination == FileDestinations.LOCAL)
if curaEnabled and isSTLFileName(filename) and local:
self.processStl(absolutePath)
if curaEnabled and isSTLFileName(filename):
self.processStl(absolutePath, destination, uploadCallback)
return filename, False
def getFutureFileName(self, file):
if not file:
return None
@ -201,8 +205,7 @@ class GcodeManager:
return self._getBasicFilename(absolutePath)
def processStl(self, absolutePath):
def processStl(self, absolutePath, destination, uploadCallback=None):
from octoprint.slicers.cura import CuraFactory
cura = CuraFactory.create_slicer()
@ -210,6 +213,7 @@ class GcodeManager:
config = self._settings.get(["cura", "config"])
slicingStart = time.time()
def stlProcessed(stlPath, gcodePath, error=None):
if error:
eventManager().fire("SlicingFailed", {"stl": self._getBasicFilename(stlPath), "gcode": self._getBasicFilename(gcodePath), "reason": error})
@ -218,13 +222,12 @@ class GcodeManager:
else:
slicingStop = time.time()
eventManager().fire("SlicingDone", {"stl": self._getBasicFilename(stlPath), "gcode": self._getBasicFilename(gcodePath), "time": "%.2f" % (slicingStop - slicingStart)})
self.processGcode(gcodePath)
self.processGcode(gcodePath, destination, uploadCallback)
eventManager().fire("SlicingStarted", {"stl": self._getBasicFilename(absolutePath), "gcode": self._getBasicFilename(gcodePath)})
cura.process_file(config, gcodePath, absolutePath, stlProcessed, [absolutePath, gcodePath])
def processGcode(self, absolutePath):
def processGcode(self, absolutePath, destination, uploadCallback=None):
if absolutePath is None:
return None
@ -238,6 +241,8 @@ class GcodeManager:
self._metadataAnalyzer.addFileToQueue(os.path.basename(absolutePath))
if uploadCallback is not None:
uploadCallback(filename, absolutePath, destination)
return filename
def getFutureFilename(self, file):

View file

@ -9,14 +9,14 @@ import copy
import os
import logging
#import logging, logging.config
import octoprint.util.comm as comm
import octoprint.util as util
from octoprint.settings import settings
from octoprint.events import eventManager
from octoprint.filemanager.destinations import FileDestinations
def getConnectionOptions():
"""
Retrieves the available ports, baudrates, prefered port and baudrate for connecting to the printer.
@ -71,6 +71,8 @@ class Printer():
self._sdPrinting = False
self._sdStreaming = False
self._sdFilelistAvailable = threading.Event()
self._sdRemoteName = None
self._streamingFinishedCallback = None
self._selectedFile = None
@ -455,9 +457,13 @@ class Printer():
self._setProgressData(0.0, 0, 0, None)
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def mcFileTransferDone(self):
def mcFileTransferDone(self, filename):
self._sdStreaming = False
if self._streamingFinishedCallback is not None:
self._streamingFinishedCallback(self._sdRemoteName, FileDestinations.SDCARD)
self._sdRemoteName = None
self._setCurrentZ(None)
self._setJobData(None, None, None)
self._setProgressData(None, None, None, None)
@ -473,35 +479,18 @@ class Printer():
return []
return self._comm.getSdFiles()
def addSdFile(self, filename, absolutePath):
from octoprint.gcodefiles import isGcodeFileName
from octoprint.gcodefiles import isSTLFileName
def addSdFile(self, filename, absolutePath, streamingFinishedCallback):
if not self._comm or self._comm.isBusy() or not self._comm.isSdReady():
logging.error("No connection to printer or printer is busy")
return
if isGcodeFileName(filename):
self.streamSdFile(filename, absolutePath)
if isSTLFileName(filename):
gcodePath = util.genGcodeFileName(absolutePath)
gcodeFileName = util.genGcodeFileName(filename)
callBackArgs = [gcodeFileName, gcodePath]
callBack = self.streamSdFile
self._gcodeManager.processStl(
absolutePath, callBack, callBackArgs)
def streamSdFile(self, filename, path):
if not self._comm or self._comm.isBusy() or not self._comm.isSdReady():
return
self._streamingFinishedCallback = streamingFinishedCallback
self.refreshSdFiles(blocking=True)
existingSdFiles = self._comm.getSdFiles()
sdFilename = util.getDosFilename(filename, existingSdFiles)
self._comm.startFileTransfer(path, sdFilename)
self._sdRemoteName = util.getDosFilename(filename, existingSdFiles)
self._comm.startFileTransfer(absolutePath, self._sdRemoteName)
def deleteSdFile(self, filename):
if not self._comm or not self._comm.isSdReady():
@ -569,6 +558,13 @@ class Printer():
}
}
def getCurrentConnection(self):
if self._comm is None:
return "Closed", None, None
port, baudrate = self._comm.getConnection()
return self._comm.getStateString(), port, baudrate
def isClosedOrError(self):
return self._comm is None or self._comm.isClosedOrError()

View file

@ -2,148 +2,255 @@
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from flask import request, jsonify
from flask import request, jsonify, make_response
from octoprint.settings import settings
from octoprint.settings import settings, valid_boolean_trues
from octoprint.printer import getConnectionOptions
from octoprint.server import printer, restricted_access, SUCCESS
from octoprint.server.ajax import ajax
import octoprint.util as util
#~~ Printer control
@ajax.route("/control/connection/options", methods=["GET"])
@ajax.route("/control/connection", methods=["GET"])
def connectionOptions():
return jsonify(getConnectionOptions())
state, port, baudrate = printer.getCurrentConnection()
current = {
"state": state,
"port": port,
"baudrate": baudrate
}
return jsonify({"current": current, "options": getConnectionOptions()})
@ajax.route("/control/connection", methods=["POST"])
@restricted_access
def connect():
if "command" in request.values.keys() and request.values["command"] == "connect":
def connectionCommand():
valid_commands = {
"connect": ["autoconnect"],
"disconnect": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
if command == "connect":
options = getConnectionOptions()
port = None
baudrate = None
if "port" in request.values.keys():
port = request.values["port"]
if "baudrate" in request.values.keys():
baudrate = request.values["baudrate"]
if "save" in request.values.keys():
if "port" in data.keys():
port = data["port"]
if port not in options["ports"]:
return make_response("Invalid port: %s" % port, 400)
if "baudrate" in data.keys():
baudrate = data["baudrate"]
if baudrate not in options["baudrates"]:
return make_response("Invalid baudrate: %d" % baudrate, 400)
if "save" in data.keys() and data["save"]:
settings().set(["serial", "port"], port)
settings().setInt(["serial", "baudrate"], baudrate)
settings().save()
if "autoconnect" in request.values.keys():
settings().setBoolean(["serial", "autoconnect"], True)
settings().save()
settings().setBoolean(["serial", "autoconnect"], data["autoconnect"])
settings().save()
printer.connect(port=port, baudrate=baudrate)
elif "command" in request.values.keys() and request.values["command"] == "disconnect":
elif command == "disconnect":
printer.disconnect()
return jsonify(SUCCESS)
@ajax.route("/control/command", methods=["POST"])
@ajax.route("/control/printer/command", methods=["POST"])
@restricted_access
def printerCommand():
if "application/json" in request.headers["Content-Type"]:
data = request.json
if not printer.isOperational():
return make_response("Printer is not operational", 403)
parameters = {}
if "parameters" in data.keys(): parameters = data["parameters"]
if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content type JSON", 400)
commands = []
if "command" in data.keys(): commands = [data["command"]]
elif "commands" in data.keys(): commands = data["commands"]
data = request.json
commandsToSend = []
for command in commands:
commandToSend = command
if len(parameters) > 0:
commandToSend = command % parameters
commandsToSend.append(commandToSend)
parameters = {}
if "parameters" in data.keys(): parameters = data["parameters"]
printer.commands(commandsToSend)
commands = []
if "command" in data.keys(): commands = [data["command"]]
elif "commands" in data.keys(): commands = data["commands"]
commandsToSend = []
for command in commands:
commandToSend = command
if len(parameters) > 0:
commandToSend = command % parameters
commandsToSend.append(commandToSend)
printer.commands(commandsToSend)
return jsonify(SUCCESS)
@ajax.route("/control/job", methods=["POST"])
@restricted_access
def printJobControl():
if "command" in request.values.keys():
if request.values["command"] == "start":
printer.startPrint()
elif request.values["command"] == "pause":
printer.togglePausePrint()
elif request.values["command"] == "cancel":
printer.cancelPrint()
def controlJob():
if not printer.isOperational():
return make_response("Printer is not operational", 403)
valid_commands = {
"start": [],
"pause": [],
"cancel": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
if command == "start":
printer.startPrint()
elif command == "pause":
printer.togglePausePrint()
elif command == "cancel":
printer.cancelPrint()
return jsonify(SUCCESS)
@ajax.route("/control/temperature", methods=["POST"])
@ajax.route("/control/printer/hotend", methods=["POST"])
@restricted_access
def setTargetTemperature():
if "temp" in request.values.keys():
# set target temperature
temp = request.values["temp"]
printer.command("M104 S" + temp)
def controlPrinterHotend():
if not printer.isOperational():
return make_response("Printer is not operational", 403)
if "bedTemp" in request.values.keys():
# set target bed temperature
bedTemp = request.values["bedTemp"]
printer.command("M140 S" + bedTemp)
valid_commands = {
"temp": ["temps"],
"offset": ["offsets"]
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
if "tempOffset" in request.values.keys():
# set target temperature offset
try:
tempOffset = float(request.values["tempOffset"])
if tempOffset >= -50 and tempOffset <= 50:
printer.setTemperatureOffset(tempOffset, None)
except:
pass
valid_targets = ["hotend", "bed"]
if "bedTempOffset" in request.values.keys():
# set target bed temperature offset
try:
bedTempOffset = float(request.values["bedTempOffset"])
if bedTempOffset >= -50 and bedTempOffset <= 50:
printer.setTemperatureOffset(None, bedTempOffset)
except:
pass
##~~ temperature
if command == "temp":
temps = data["temps"]
# make sure the targets are valid and the values are numbers
validated_values = {}
for type, value in temps.iteritems():
if not type in valid_targets:
return make_response("Invalid target for setting temperature: %s" % type, 400)
if not isinstance(value, (int, long, float)):
return make_response("Not a number for %s: %r" % (type, value), 400)
validated_values[type] = value
# perform the actual temperature commands
# TODO make this a generic method call (printer.setTemperature(type, value)) to get rid of gcode here
if "hotend" in validated_values:
printer.command("M104 S%f" % validated_values["hotend"])
if "bed" in validated_values:
printer.command("M140 S%f" % validated_values["bed"])
##~~ temperature offset
elif command == "offset":
offsets = data["offsets"]
# make sure the targets are valid, the values are numbers and in the range [-50, 50]
validated_values = {}
for type, value in offsets.iteritems():
if not type in valid_targets:
return make_response("Invalid target for setting temperature: %s" % type, 400)
if not isinstance(value, (int, long, float)):
return make_response("Not a number for %s: %r" % (type, value), 400)
if not -50 <= value <= 50:
return make_response("Offset %s not in range [-50, 50]: %f" % (type, value), 400)
validated_values[type] = value
# set the offsets
if "hotend" in validated_values and "bed" in validated_values:
printer.setTemperatureOffset(validated_values["hotend"], validated_values["bed"])
elif "hotend" in validated_values:
printer.setTemperatureOffset(validated_values["hotend"], None)
elif "bed" in validated_values:
printer.setTemperatureOffset(None, validated_values["bed"])
return jsonify(SUCCESS)
@ajax.route("/control/jog", methods=["POST"])
@ajax.route("/control/printer/printhead", methods=["POST"])
@restricted_access
def jog():
def controlPrinterPrinthead():
if not printer.isOperational() or printer.isPrinting():
# do not jog when a print job is running or we don't have a connection
return jsonify(SUCCESS)
return make_response("Printer is not operational or currently printing", 403)
(movementSpeedX, movementSpeedY, movementSpeedZ, movementSpeedE) = settings().get(["printerParameters", "movementSpeed", ["x", "y", "z", "e"]])
if "x" in request.values.keys():
# jog x
x = request.values["x"]
printer.commands(["G91", "G1 X%s F%d" % (x, movementSpeedX), "G90"])
if "y" in request.values.keys():
# jog y
y = request.values["y"]
printer.commands(["G91", "G1 Y%s F%d" % (y, movementSpeedY), "G90"])
if "z" in request.values.keys():
# jog z
z = request.values["z"]
printer.commands(["G91", "G1 Z%s F%d" % (z, movementSpeedZ), "G90"])
if "homeXY" in request.values.keys():
# home x/y
printer.command("G28 X0 Y0")
if "homeZ" in request.values.keys():
# home z
printer.command("G28 Z0")
if "extrude" in request.values.keys():
# extrude/retract
length = request.values["extrude"]
printer.commands(["G91", "G1 E%s F%d" % (length, movementSpeedE), "G90"])
valid_commands = {
"jog": [],
"home": ["axes"]
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
movementSpeed = settings().get(["printerParameters", "movementSpeed", ["x", "y", "z"]], asdict=True)
valid_axes = ["x", "y", "z"]
##~~ jog command
if command == "jog":
# validate all jog instructions, make sure that the values are numbers
validated_values = {}
for axis in valid_axes:
if axis in data:
value = data[axis]
if not isinstance(value, (int, long, float)):
return make_response("Not a number for axis %s: %r" % (axis, value), 400)
validated_values[axis] = value
# execute the jog commands
for axis, value in validated_values.iteritems():
# TODO make this a generic method call (printer.jog(axis, value)) to get rid of gcode here
printer.commands(["G91", "G1 %s%.4f F%d" % (axis.upper(), value, movementSpeed[axis]), "G90"])
##~~ home command
elif command == "home":
validated_values = []
axes = data["axes"]
for axis in axes:
if not axis in valid_axes:
return make_response("Invalid axis: %s" % axis, 400)
validated_values.append(axis)
# execute the home command
# TODO make this a generic method call (printer.home(axis, ...)) to get rid of gcode here
printer.commands(["G91", "G1 %s" % " ".join(map(lambda x: "%s0" % x.upper(), validated_values)), "G90"])
return jsonify(SUCCESS)
@ajax.route("/control/printer/feeder", methods=["POST"])
@restricted_access
def controlPrinterFeeder():
if not printer.isOperational() or printer.isPrinting():
# do not jog when a print job is running or we don't have a connection
return make_response("Printer is not operational or currently printing", 403)
valid_commands = {
"extrude": ["amount"]
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
extrusionSpeed = settings().get(["printerParameters", "movementSpeed", "e"])
if command == "extrude":
amount = data["amount"]
if not isinstance(amount, (int, long, float)):
return make_response("Not a number for extrusion amount: %r" % amount, 400)
# TODO make this a generic method call (printer.extruder([hotend,] amount)) to get rid of gcode here
printer.commands(["G91", "G1 E%s F%d" % (data["amount"], extrusionSpeed), "G90"])
return jsonify(SUCCESS)
@ -158,16 +265,23 @@ def getCustomControls():
@restricted_access
def sdCommand():
if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting():
return jsonify(SUCCESS)
return make_response("SD support is disabled", 403)
if "command" in request.values.keys():
command = request.values["command"]
if command == "init":
printer.initSdCard()
elif command == "refresh":
printer.refreshSdFiles()
elif command == "release":
printer.releaseSdCard()
valid_commands = {
"init": [],
"refresh": [],
"release": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
if command == "init":
printer.initSdCard()
elif command == "refresh":
printer.refreshSdFiles()
elif command == "release":
printer.releaseSdCard()
return jsonify(SUCCESS)

View file

@ -18,33 +18,51 @@ from octoprint.server.ajax import ajax
@ajax.route("/gcodefiles", methods=["GET"])
def readGcodeFiles():
files = gcodeManager.getAllFileData()
sdFileList = printer.getSdFiles()
if sdFileList is not None:
for sdFile in sdFileList:
files.append({
"name": sdFile,
"size": "n/a",
"bytes": 0,
"date": "n/a",
"origin": "sd"
})
files = _getFileList(FileDestinations.LOCAL)
files.extend(_getFileList(FileDestinations.SDCARD))
return jsonify(files=files, free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
@ajax.route("/gcodefiles/<path:filename>", methods=["GET"])
def readGcodeFile(filename):
return redirectToTornado(request, url_for("index") + "downloads/gcode/" + filename)
@ajax.route("/gcodefiles/<string:target>", methods=["GET"])
def readGcodeFilesForTarget(target):
if target not in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
return make_response("Invalid target: %s" % target, 400)
return jsonify(files=_getFileList(target), free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
@ajax.route("/gcodefiles/upload", methods=["POST"])
def _getFileList(target):
if target == FileDestinations.SDCARD:
sdFileList = printer.getSdFiles()
files = []
if sdFileList is not None:
for sdFile in sdFileList:
files.append({
"name": sdFile,
"size": "n/a",
"bytes": 0,
"date": "n/a",
"origin": FileDestinations.SDCARD
})
else:
files = gcodeManager.getAllFileData()
return files
@ajax.route("/gcodefiles/<string:target>", methods=["POST"])
@restricted_access
def uploadGcodeFile():
def uploadGcodeFile(target):
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
return make_response("Invalid target: %s" % target, 400)
if "gcode_file" in request.files.keys():
file = request.files["gcode_file"]
sd = "target" in request.values.keys() and request.values["target"] == "sd";
sd = target == FileDestinations.SDCARD
selectAfterUpload = "select" in request.values.keys() and request.values["select"] in valid_boolean_trues
printAfterSelect = "print" in request.values.keys() and request.values["print"] in valid_boolean_trues
# determine current job
currentFilename = None
currentSd = None
currentJob = printer.getCurrentJob()
@ -52,82 +70,126 @@ def uploadGcodeFile():
currentFilename = currentJob["filename"]
currentSd = currentJob["sd"]
# determine future filename of file to be uploaded, abort if it can't be uploaded
futureFilename = gcodeManager.getFutureFilename(file)
if futureFilename is None or (not settings().getBoolean(["cura", "enabled"]) and not gcodefiles.isGcodeFileName(futureFilename)):
return make_response("Can not upload file %s, wrong format?" % file.filename, 400)
# prohibit overwriting currently selected file while it's being printed
if futureFilename == currentFilename and sd == currentSd and printer.isPrinting() or printer.isPaused():
# trying to overwrite currently selected file, but it is being printed
return make_response("Trying to overwrite file that is currently being printed: %s" % currentFilename, 403)
filename = None
def fileProcessingFinished(filename, absFilename, destination):
"""
Callback for when the file processing (upload, optional slicing, addition to analysis queue) has
finished.
Depending on the file's destination triggers either streaming to SD card or directly calls selectOrPrint.
"""
sd = destination == FileDestinations.SDCARD
if sd:
printer.addSdFile(filename, absFilename, selectAndOrPrint)
else:
selectAndOrPrint(absFilename, destination)
def selectAndOrPrint(nameToSelect, destination):
"""
Callback for when the file is ready to be selected and optionally printed. For SD file uploads this only
the case after they have finished streaming to the printer, which is why this callback is also used
for the corresponding call to addSdFile.
Selects the just uploaded file if either selectAfterUpload or printAfterSelect are True, or if the
exact file is already selected, such reloading it.
"""
sd = destination == FileDestinations.SDCARD
if selectAfterUpload or printAfterSelect or (currentFilename == filename and currentSd == sd):
printer.selectFile(nameToSelect, sd, printAfterSelect)
destination = FileDestinations.SDCARD if sd else FileDestinations.LOCAL
filename, done = gcodeManager.addFile(file, destination)
filename, done = gcodeManager.addFile(file, destination, fileProcessingFinished)
if filename is None:
return make_response("Could not upload the file %s" % file.filename, 500)
absFilename = gcodeManager.getAbsolutePath(filename)
if sd:
printer.addSdFile(filename, absFilename)
if currentFilename == filename and currentSd == sd:
# reload file as it was updated
if sd:
printer.selectFile(filename, sd, False)
else:
printer.selectFile(absFilename, sd, False)
eventManager.fire("Upload", filename)
return jsonify(files=gcodeManager.getAllFileData(), filename=filename, done=done)
return jsonify(files=gcodeManager.getAllFileData(), filename=filename, done=done)
else:
return make_response("No gcode_file included", 400)
@ajax.route("/gcodefiles/load", methods=["POST"])
@ajax.route("/gcodefiles/local/<path:filename>", methods=["GET"])
def readGcodeFile(filename):
return redirectToTornado(request, url_for("index") + "downloads/gcode/" + filename)
@ajax.route("/gcodefiles/<string:target>/<path:filename>", methods=["POST"])
@restricted_access
def loadGcodeFile():
if "filename" in request.values.keys():
def gcodeFileCommand(filename, target):
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
return make_response("Invalid target: %s" % target, 400)
# valid file commands, dict mapping command name to mandatory parameters
valid_commands = {
"load": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
if command == "load":
# selects/loads a file
printAfterLoading = False
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
if "print" in data.keys() and data["print"] in valid_boolean_trues:
printAfterLoading = True
sd = False
if "target" in request.values.keys() and request.values["target"] == "sd":
filename = request.values["filename"]
if target == FileDestinations.SDCARD:
filenameToSelect = filename
sd = True
else:
filename = gcodeManager.getAbsolutePath(request.values["filename"])
printer.selectFile(filename, sd, printAfterLoading)
return jsonify(SUCCESS)
filenameToSelect = gcodeManager.getAbsolutePath(filename)
printer.selectFile(filenameToSelect, sd, printAfterLoading)
return jsonify(SUCCESS)
return make_response("Command %s is currently not implemented" % command, 400)
@ajax.route("/gcodefiles/delete", methods=["POST"])
@ajax.route("/gcodefiles/<string:target>/<path:filename>", methods=["DELETE"])
@restricted_access
def deleteGcodeFile():
if "filename" in request.values.keys():
filename = request.values["filename"]
sd = "target" in request.values.keys() and request.values["target"] == "sd"
def deleteGcodeFile(filename, target):
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
return make_response("Invalid target: %s" % target, 400)
currentJob = printer.getCurrentJob()
currentFilename = None
currentSd = None
if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
currentFilename = currentJob["filename"]
currentSd = currentJob["sd"]
sd = target == FileDestinations.SDCARD
if currentFilename is not None and filename == currentFilename and not (printer.isPrinting() or printer.isPaused()):
printer.unselectFile()
currentJob = printer.getCurrentJob()
currentFilename = None
currentSd = None
if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
currentFilename = currentJob["filename"]
currentSd = currentJob["sd"]
if not (currentFilename == filename and currentSd == sd and (printer.isPrinting() or printer.isPaused())):
if sd:
printer.deleteSdFile(filename)
else:
gcodeManager.removeFile(filename)
if currentFilename is not None and filename == currentFilename and not (printer.isPrinting() or printer.isPaused()):
printer.unselectFile()
if not (currentFilename == filename and currentSd == sd and (printer.isPrinting() or printer.isPaused())):
if sd:
printer.deleteSdFile(filename)
else:
gcodeManager.removeFile(filename)
return readGcodeFiles()
@ajax.route("/gcodefiles/refresh", methods=["POST"])
@ajax.route("/gcodefiles/<string:target>/refresh", methods=["POST"])
@restricted_access
def refreshFiles():
printer.updateSdFiles()
def refreshFiles(target):
if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]:
return make_response("Invalid target: %s" % target, 400)
if target == FileDestinations.SDCARD:
printer.updateSdFiles()
return jsonify(SUCCESS)

View file

@ -123,3 +123,34 @@ def changePasswordForUser(username):
else:
return make_response(("Forbidden", 403, []))
@ajax.route("/users/<username>/apikey", methods=["DELETE"])
@restricted_access
def deleteApikeyForUser(username):
if userManager is None:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
try:
userManager.deleteApikey(username)
except users.UnknownUser:
return make_response(("Unknown user: %s" % username, 404, []))
return jsonify(SUCCESS)
else:
return make_response(("Forbidden", 403, []))
@ajax.route("/users/<username>/apikey", methods=["POST"])
@restricted_access
def generateApikeyForUser(username):
if userManager is None:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
try:
apikey = userManager.generateApiKey(username)
except users.UnknownUser:
return make_response(("Unknown user: %s" % username, 404, []))
return jsonify({"apikey": apikey})
else:
return make_response(("Forbidden", 403, []))

View file

@ -7,36 +7,30 @@ import logging
from flask import Blueprint, request, jsonify, abort
from octoprint.server import printer, gcodeManager, SUCCESS
from octoprint.settings import settings, valid_boolean_trues
from octoprint.server.util import api_access
from octoprint.settings import valid_boolean_trues
from octoprint.filemanager.destinations import FileDestinations
import octoprint.gcodefiles as gcodefiles
api = Blueprint("api", __name__)
#-- very simple api routines
@api.route("/load", methods=["POST"])
@api_access
def apiLoad():
logger = logging.getLogger(__name__)
if not settings().get(["api", "enabled"]):
abort(401)
if not "apikey" in request.values.keys():
abort(401)
if request.values["apikey"] != settings().get(["api", "key"]):
abort(403)
if not "file" in request.files.keys():
abort(400)
# Perform an upload
file = request.files["file"]
if not gcodefiles.isGcodeFileName(file.filename):
f = request.files["file"]
if not gcodefiles.isGcodeFileName(f.filename):
abort(400)
destination = FileDestinations.LOCAL
filename, done = gcodeManager.addFile(file, destination)
filename, done = gcodeManager.addFile(f, destination)
if filename is None:
logger.warn("Upload via API failed")
abort(500)
@ -51,19 +45,11 @@ def apiLoad():
return jsonify(SUCCESS)
@api.route("/state", methods=["GET"])
@api_access
def apiPrinterState():
if not settings().get(["api", "enabled"]):
abort(401)
if not "apikey" in request.values.keys():
abort(401)
if request.values["apikey"] != settings().get(["api", "key"]):
abort(403)
currentData = printer.getCurrentData()
currentData.update({
"temperatures": printer.getCurrentTemperatures()
"temperatures": printer.getCurrentTemperatures()
})
return jsonify(currentData)

View file

@ -1,7 +1,7 @@
from flask.ext.principal import identity_changed, Identity
from tornado.web import StaticFileHandler, HTTPError
from flask import url_for, make_response
from flask.ext.login import login_required
from flask import url_for, make_response, request, current_app
from flask.ext.login import login_required, login_user
from werkzeug.utils import redirect
from sockjs.tornado import SockJSConnection
@ -15,32 +15,70 @@ import threading
import logging
from functools import wraps
from octoprint.server import userManager
from octoprint.settings import settings
import octoprint.timelapse
import octoprint.server
from octoprint.users import ApiUser
def restricted_access(func):
def restricted_access(func, apiEnabled=True):
"""
If you decorate a view with this, it will ensure that first setup has been
done for OctoPrint's Access Control plus that any conditions of the
login_required decorator are met.
login_required decorator are met. It also allows to login using the masterkey or any
of the user's apikeys if API access is enabled globally and for the decorated view.
If OctoPrint's Access Control has not been setup yet (indicated by the "firstRun"
flag from the settings being set to True and the userManager not indicating
that it's user database has been customized from default), the decorator
will cause a HTTP 403 status code to be returned by the decorated resource.
If an API key is provided and it matches a known key, the user will be logged in and
the view will be called directly. If the provided key doesn't match any known key,
a HTTP 403 status code will be returned by the decorated resource.
Otherwise the result of calling login_required will be returned.
"""
@wraps(func)
def decorated_view(*args, **kwargs):
if settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()):
# if OctoPrint hasn't been set up yet, abort
if settings().getBoolean(["server", "firstRun"]) and (octoprint.server.userManager is None or not octoprint.server.userManager.hasBeenCustomized()):
return make_response("OctoPrint isn't setup yet", 403)
# if API is globally enabled, enabled for this request and an api key is provided, try to use that
if settings().get(["api", "enabled"]) and apiEnabled and "apikey" in request.values.keys():
apikey = request.values["apikey"]
user = None
if apikey == settings().get(["api", "key"]):
# master key was used
user = ApiUser()
else:
# user key might have been used
user = octoprint.server.userManager.findUser(apikey=apikey)
if user is None:
make_response("Invalid API key", 403)
if login_user(user, remember=False):
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
return func(*args, **kwargs)
# call regular login_required decorator
return login_required(func)(*args, **kwargs)
return decorated_view
def api_access(func):
@wraps(func)
def decorated_view(*args, **kwargs):
if not settings().get(["api", "enabled"]):
make_response("API disabled", 401)
if not "apikey" in request.values.keys():
make_response("No API key provided", 401)
if request.values["apikey"] != settings().get(["api", "key"]):
make_response("Invalid API key", 403)
return func(args, kwargs)
return decorated_view
#~~ Printer state
@ -286,7 +324,7 @@ class ReverseProxied(object):
def redirectToTornado(request, target):
requestUrl = request.url
appBaseUrl = requestUrl[:requestUrl.find(url_for("ajax.base"))]
appBaseUrl = requestUrl[:requestUrl.find(url_for("index") + "/ajax")]
redirectUrl = appBaseUrl + target
if "?" in requestUrl:

View file

@ -187,7 +187,7 @@ class Settings(object):
#~~ getter
def get(self, path):
def get(self, path, asdict=False):
if len(path) == 0:
return None
@ -211,17 +211,28 @@ class Settings(object):
else:
keys = k
results = []
if asdict:
results = {}
else:
results = []
for key in keys:
if key in config.keys():
results.append(config[key])
value = config[key]
elif key in defaults:
results.append(defaults[key])
value = defaults[key]
else:
results.append(None)
value = None
if asdict:
results[key] = value
else:
results.append(value)
if not isinstance(k, (list, tuple)):
return results.pop()
if asdict:
return results.values().pop()
else:
return results.pop()
else:
return results

View file

@ -19,6 +19,10 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
//~~ item handling
self.refresh = function() {
self._updateItems();
}
self.updateItems = function(items) {
self.allItems = items;
self._updateItems();

View file

@ -86,9 +86,9 @@ $(function() {
function enable_local_dropzone() {
$("#gcode_upload").fileupload({
url: AJAX_BASEURL + "gcodefiles/local",
dataType: "json",
dropZone: localTarget,
formData: {target: "local"},
done: gcode_upload_done,
fail: gcode_upload_fail,
progressall: gcode_upload_progress
@ -97,9 +97,9 @@ $(function() {
function disable_local_dropzone() {
$("#gcode_upload").fileupload({
url: AJAX_BASEURL + "gcodefiles/local",
dataType: "json",
dropZone: null,
formData: {target: "local"},
done: gcode_upload_done,
fail: gcode_upload_fail,
progressall: gcode_upload_progress
@ -108,9 +108,9 @@ $(function() {
function enable_sd_dropzone() {
$("#gcode_upload_sd").fileupload({
url: AJAX_BASEURL + "gcodefiles/sdcard",
dataType: "json",
dropZone: $("#drop_sd"),
formData: {target: "sd"},
done: gcode_upload_done,
fail: gcode_upload_fail,
progressall: gcode_upload_progress
@ -119,6 +119,7 @@ $(function() {
function disable_sd_dropzone() {
$("#gcode_upload_sd").fileupload({
url: AJAX_BASEURL + "gcodefiles/sdcard",
dataType: "json",
dropZone: null,
formData: {target: "sd"},

View file

@ -9,6 +9,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
self.selectedPort = ko.observable(undefined);
self.selectedBaudrate = ko.observable(undefined);
self.saveSettings = ko.observable(undefined);
self.autoconnect = ko.observable(undefined);
self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined);
@ -29,7 +30,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "control/connection/options",
url: AJAX_BASEURL + "control/connection",
method: "GET",
dataType: "json",
success: function(response) {
@ -39,13 +40,18 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
}
self.fromResponse = function(response) {
self.portOptions(response.ports);
self.baudrateOptions(response.baudrates);
var ports = response.options.ports;
var baudrates = response.options.baudrates;
var portPreference = response.options.portPreference;
var baudratePreference = response.options.baudratePreference;
if (!self.selectedPort() && response.ports && response.ports.indexOf(response.portPreference) >= 0)
self.selectedPort(response.portPreference);
if (!self.selectedBaudrate() && response.baudrates && response.baudrates.indexOf(response.baudratePreference) >= 0)
self.selectedBaudrate(response.baudratePreference);
self.portOptions(ports);
self.baudrateOptions(baudrates);
if (!self.selectedPort() && ports && ports.indexOf(portPreference) >= 0)
self.selectedPort(portPreference);
if (!self.selectedBaudrate() && baudrates && baudrates.indexOf(baudratePreference) >= 0)
self.selectedBaudrate(baudratePreference);
self.saveSettings(false);
}
@ -86,7 +92,8 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
var data = {
"command": "connect",
"port": self.selectedPort(),
"baudrate": self.selectedBaudrate()
"baudrate": self.selectedBaudrate(),
"autoconnect": self.settings.serial_autoconnect()
};
if (self.saveSettings())
@ -96,19 +103,20 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
url: AJAX_BASEURL + "control/connection",
type: "POST",
dataType: "json",
data: data
})
self.settings.serial_port(self.selectedPort())
self.settings.serial_baudrate(self.selectedBaudrate())
self.settings.saveData();
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(data),
success: function(response) {
self.settings.requestData()
}
});
} else {
self.requestData();
$.ajax({
url: AJAX_BASEURL + "control/connection",
type: "POST",
dataType: "json",
data: {"command": "disconnect"}
contentType: "application/json; charset=UTF-8",
data: JSON.stringify({"command": "disconnect"})
})
}
}

View file

@ -80,26 +80,37 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
self.sendJogCommand = function(axis, multiplier, distance) {
if (typeof distance === "undefined")
distance = $('#jog_distance button.active').data('distance');
if (self.settings.getPrinterInvertAxis(axis)) {
multiplier *= -1;
}
var data = {
"command": "jog"
}
data[axis] = distance * multiplier;
$.ajax({
url: AJAX_BASEURL + "control/jog",
url: AJAX_BASEURL + "control/printer/printhead",
type: "POST",
dataType: "json",
data: axis + "=" + ( distance * multiplier )
})
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(data)
});
}
self.sendHomeCommand = function(axis) {
var data = {
"command": "home",
"axes": axis
}
$.ajax({
url: AJAX_BASEURL + "control/jog",
url: AJAX_BASEURL + "control/printer/printhead",
type: "POST",
dataType: "json",
data: "home" + axis
})
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(data)
});
}
self.sendExtrudeCommand = function() {
@ -114,12 +125,14 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
var length = self.extrusionAmount();
if (!length)
length = 5;
$.ajax({
url: AJAX_BASEURL + "control/jog",
url: AJAX_BASEURL + "control/printer/feeder",
type: "POST",
dataType: "json",
data: "extrude=" + (dir * length)
})
contentType: "application/json; charset=UTF-8",
data: JSON.stringify({command: "extrude", amount: (dir * length)})
});
}
self.sendCustomCommand = function(command) {
@ -143,11 +156,11 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
}
}
if (!data)
if (data === undefined)
return;
$.ajax({
url: AJAX_BASEURL + "control/command",
url: AJAX_BASEURL + "control/printer/command",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -43,10 +43,10 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0);
},
"sd": function(file) {
return file["origin"] && file["origin"] == "sd";
return file["origin"] && file["origin"] == "sdcard";
},
"local": function(file) {
return !(file["origin"] && file["origin"] == "sd");
return !(file["origin"] && file["origin"] == "sdcard");
}
},
"name",
@ -127,11 +127,19 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
var file = self.listHelper.getItem(function(item) {return item.name == filename});
if (!file) return;
var origin;
if (file.origin === undefined) {
origin = "local";
} else {
origin = file.origin;
}
$.ajax({
url: AJAX_BASEURL + "gcodefiles/load",
url: AJAX_BASEURL + "gcodefiles/" + origin + "/" + filename,
type: "POST",
dataType: "json",
data: {filename: filename, print: printAfterLoad, target: file.origin}
contentType: "application/json; charset=UTF-8",
data: JSON.stringify({command: "load", print: printAfterLoad})
})
}
@ -139,11 +147,16 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
var file = self.listHelper.getItem(function(item) {return item.name == filename});
if (!file) return;
var origin;
if (file.origin === undefined) {
origin = "local";
} else {
origin = file.origin;
}
$.ajax({
url: AJAX_BASEURL + "gcodefiles/delete",
type: "POST",
dataType: "json",
data: {filename: filename, target: file.origin},
url: AJAX_BASEURL + "gcodefiles/" + origin + "/" + filename,
type: "DELETE",
success: self.fromResponse
})
}

View file

@ -147,7 +147,8 @@ function PrinterStateViewModel(loginStateViewModel) {
url: AJAX_BASEURL + "control/job",
type: "POST",
dataType: "json",
data: {command: command}
contentType: "application/json; charset=UTF-8",
data: JSON.stringify({command: command})
});
}
}

View file

@ -167,47 +167,64 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
self.setTempFromProfile = function(profile) {
if (!profile)
return;
self._updateTemperature(profile.extruder, "temp");
self._sendHotendCommand("temp", "hotend", profile.extruder);
}
self.setTemp = function() {
self._updateTemperature(self.newTemp(), "temp", function(){self.targetTemp(self.newTemp()); self.newTemp("");});
self._sendHotendCommand("temp", "hotend", self.newTemp(), function() {self.targetTemp(self.newTemp()); self.newTemp("");});
};
self.setTempToZero = function() {
self._updateTemperature(0, "temp", function(){self.targetTemp(0); self.newTemp("");});
self._sendHotendCommand("temp", "hotend", 0, function() {self.targetTemp(0); self.newTemp("");});
}
self.setTempOffset = function() {
self._updateTemperature(self.newTempOffset(), "tempOffset", function() {self.tempOffset(self.newTempOffset()); self.newTempOffset("");});
self._sendHotendCommand("offset", "hotend", self.newTempOffset(), function() {self.tempOffset(self.newTempOffset()); self.newTempOffset("");});
}
self.setBedTempFromProfile = function(profile) {
self._updateTemperature(profile.bed, "bedTemp");
if (!profile)
return;
self._sendHotendCommand("temp", "bed", profile.bed);
}
self.setBedTemp = function() {
self._updateTemperature(self.newBedTemp(), "bedTemp", function() {self.bedTargetTemp(self.newBedTemp()); self.newBedTemp("");});
self._sendHotendCommand("temp", "bed", self.newBedTemp(), function() {self.bedTargetTemp(self.newBedTemp()); self.newBedTemp("");});
};
self.setBedTempToZero = function() {
self._updateTemperature(0, "bedTemp", function() {self.bedTargetTemp(0); self.newBedTemp("");});
self._sendHotendCommand("temp", "bed", 0, function() {self.bedTargetTemp(0); self.newBedTemp("");});
}
self.setBedTempOffset = function() {
self._updateTemperature(self.newBedTempOffset(), "bedTempOffset", function() {self.bedTempOffset(self.newBedTempOffset()); self.newBedTempOffset("");});
self._sendHotendCommand("offset", "bed", self.newBedTempOffset(), function() {self.bedTempOffset(self.newBedTempOffset()); self.newBedTempOffset("");});
}
self._updateTemperature = function(temp, type, callback) {
var data = {};
data[type] = temp;
self._sendHotendCommand = function(command, type, temp, callback) {
var group;
if ("temp" == command) {
group = "temps";
} else if ("offset" == command) {
group = "offsets";
} else {
return;
}
var data = {
"command": command
};
data[group] = {};
data[group][type] = parseInt(temp);
$.ajax({
url: AJAX_BASEURL + "control/temperature",
url: AJAX_BASEURL + "control/printer/hotend",
type: "POST",
data: data,
dataType: "json",
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(data),
success: function() { if (callback !== undefined) callback(); }
});
}
self.handleEnter = function(event, type) {

View file

@ -28,6 +28,7 @@ function UsersViewModel(loginStateViewModel) {
self.editorUsername = ko.observable(undefined);
self.editorPassword = ko.observable(undefined);
self.editorRepeatedPassword = ko.observable(undefined);
self.editorApikey = ko.observable(undefined);
self.editorAdmin = ko.observable(undefined);
self.editorActive = ko.observable(undefined);
@ -36,10 +37,12 @@ function UsersViewModel(loginStateViewModel) {
self.editorUsername(undefined);
self.editorAdmin(undefined);
self.editorActive(undefined);
self.editorApikey(undefined);
} else {
self.editorUsername(newValue.name);
self.editorAdmin(newValue.admin);
self.editorActive(newValue.active);
self.editorApikey(newValue.apikey);
}
self.editorPassword(undefined);
self.editorRepeatedPassword(undefined);
@ -122,6 +125,27 @@ function UsersViewModel(loginStateViewModel) {
});
}
self.confirmGenerateApikey = function() {
if (!CONFIG_ACCESS_CONTROL) return;
self.generateApikey(self.currentUser().name, function(response) {
self._updateApikey(response.apikey);
})
}
self.confirmDeleteApikey = function() {
if (!CONFIG_ACCESS_CONTROL) return;
self.deleteApikey(self.currentUser().name, function() {
self._updateApikey(undefined);
})
}
self._updateApikey = function(apikey) {
self.editorApikey(apikey);
self.requestData();
}
//~~ AJAX calls
self.addUser = function(user, callback) {
@ -187,4 +211,24 @@ function UsersViewModel(loginStateViewModel) {
success: callback
});
}
self.generateApikey = function(username, callback) {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users/" + username + "/apikey",
type: "POST",
success: callback
});
}
self.deleteApikey = function(username, callback) {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users/" + username + "/apikey",
type: "DELETE",
success: callback
});
}
}

View file

@ -210,18 +210,18 @@
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser() || !$root.isSdReady()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload to SD</span>
<input id="gcode_upload_sd" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser() && isSdReady()">
<input id="gcode_upload_sd" type="file" name="gcode_file" class="fileinput-button" data-bind="enable: loginState.isUser() && isSdReady()">
</span>
{% else %}
<span class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload</span>
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload" data-bind="enable: loginState.isUser()">
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
{% endif %}
</div>
@ -352,7 +352,7 @@
</div>
<div>
<button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('x',-1) }"><i class="icon-arrow-left"></i></button>
<button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand('XY') }"><i class="icon-home"></i></button>
<button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand(['x', 'y']) }"><i class="icon-home"></i></button>
<button class="btn box pull-left" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('x',1) }"><i class="icon-arrow-right"></i></button>
</div>
<div>
@ -366,7 +366,7 @@
<button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('z',1) }"><i class="icon-arrow-up"></i></button>
</div>
<div>
<button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand('Z') }"><i class="icon-home"></i></button>
<button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendHomeCommand(['z']) }"><i class="icon-home"></i></button>
</div>
<div>
<button class="btn box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendJogCommand('z',-1) }"><i class="icon-arrow-down"></i></button>

View file

@ -382,7 +382,7 @@
</thead>
<tbody data-bind="foreach: users.listHelper.paginatedItems">
<tr>
<td class="settings_users_name" data-bind="text: name"></td>
<td class="settings_users_name"><span data-bind="text: name"></span><span class="muted" data-bind="visible: $root.api_enabled() && apikey"><br /><small>Apikey: <span data-bind="text: apikey"></span></small></span></td>
<td class="settings_users_active"><i data-bind="css: { 'icon-check': active, 'icon-check-empty': !active }"></i></td>
<td class="settings_users_admin"><i data-bind="css: { 'icon-check': admin, 'icon-check-empty': !admin }"></i></td>
<td class="settings_users_actions" class="system_users_action">
@ -504,6 +504,20 @@
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
</div>
</div>
<fieldset data-bind="visible: api_enabled">
<legend>Apikey</legend>
<div class="control-group">
<label class="control-label">Current Apikey</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level uneditable-input" data-bind="value: $root.users.editorApikey, attr: {placeholder: 'N/A'}">
<a class="btn" title="Generate new Apikey" data-bind="click: function() { $root.users.confirmGenerateApikey(); }"><i class="icon-refresh"></i></a>
<a class="btn btn-danger" title="Delete Apikey" data-bind="click: function() { $root.users.confirmDeleteApikey(); }"><i class="icon-trash"></i></a>
</div>
</div>
</div>
</fieldset>
</form>
</div>
<div class="modal-footer">
@ -511,6 +525,7 @@
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmChangePassword(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
</div>
</div>
</div>
{% endif %}

View file

@ -7,6 +7,7 @@ from flask.ext.principal import Identity
import hashlib
import os
import yaml
import uuid
from octoprint.settings import settings
@ -70,7 +71,10 @@ class FilebasedUserManager(UserManager):
data = yaml.safe_load(f)
for name in data.keys():
attributes = data[name]
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"])
apikey = None
if "apikey" in attributes:
apikey = attributes["apikey"]
self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"], apikey)
else:
self._customized = False
@ -84,7 +88,8 @@ class FilebasedUserManager(UserManager):
data[name] = {
"password": user._passwordHash,
"active": user._active,
"roles": user._roles
"roles": user._roles,
"apikey": user._apikey
}
with open(self._userfile, "wb") as f:
@ -92,11 +97,11 @@ class FilebasedUserManager(UserManager):
self._dirty = False
self._load()
def addUser(self, username, password, active=False, roles=["user"]):
def addUser(self, username, password, active=False, roles=["user"], apikey=None):
if username in self._users.keys():
raise UserAlreadyExists(username)
self._users[username] = User(username, UserManager.createPasswordHash(password), active, roles)
self._users[username] = User(username, UserManager.createPasswordHash(password), active, roles, apikey)
self._dirty = True
self._save()
@ -154,6 +159,25 @@ class FilebasedUserManager(UserManager):
self._dirty = True
self._save()
def generateApiKey(self, username):
if not username in self._users.keys():
raise UnknownUser(username)
user = self._users[username]
user._apikey = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes)
self._dirty = True
self._save()
return user._apikey
def deleteApikey(self, username):
if not username in self._users.keys():
raise UnknownUser(username)
user = self._users[username]
user._apikey = None
self._dirty = True
self._save()
def removeUser(self, username):
if not username in self._users.keys():
raise UnknownUser(username)
@ -162,14 +186,19 @@ class FilebasedUserManager(UserManager):
self._dirty = True
self._save()
def findUser(self, username=None):
if username is None:
return None
def findUser(self, username=None, apikey=None):
if username is not None:
if username not in self._users.keys():
return None
if username not in self._users.keys():
return self._users[username]
elif apikey is not None:
for user in self._users.values():
if apikey == user._apikey:
return user
return None
else:
return None
return self._users[username]
def getAllUsers(self):
return map(lambda x: x.asDict(), self._users.values())
@ -194,18 +223,20 @@ class UnknownRole(Exception):
##~~ User object
class User(UserMixin):
def __init__(self, username, passwordHash, active, roles):
def __init__(self, username, passwordHash, active, roles, apikey=None):
self._username = username
self._passwordHash = passwordHash
self._active = active
self._roles = roles
self._apikey = apikey
def asDict(self):
return {
"name": self._username,
"active": self.is_active(),
"admin": self.is_admin(),
"user": self.is_user()
"user": self.is_user(),
"apikey": self._apikey
}
def check_password(self, passwordHash):
@ -241,3 +272,11 @@ class DummyIdentity(Identity):
def dummy_identity_loader():
return DummyIdentity()
##~~ Apiuser object to use when api key is used to access the API
class ApiUser(User):
def __init__(self):
User.__init__(self, "api", "", True, UserManager.valid_roles)

View file

@ -8,6 +8,7 @@ import sys
import time
import re
import tempfile
from flask import make_response
from octoprint.settings import settings
@ -188,3 +189,20 @@ def silentRemove(file):
os.remove(file)
except OSError:
pass
def getJsonCommandFromRequest(request, valid_commands):
if not "application/json" in request.headers["Content-Type"]:
return None, None, make_response("Expected content-type JSON", 400)
data = request.json
if not "command" in data.keys() or not data["command"] in valid_commands.keys():
return None, None, make_response("Expected valid command", 400)
command = data["command"]
for parameter in valid_commands[command]:
if not parameter in data:
return None, None, make_response("Mandatory parameter %s missing for command %s" % (parameter, command), 400)
return command, data, None

View file

@ -291,6 +291,9 @@ class MachineCom(object):
def getOffsets(self):
return (self._tempOffset, self._bedTempOffset)
def getConnection(self):
return self._port, self._baudrate
##~~ external interface
def close(self, isError = False):
@ -826,8 +829,8 @@ class MachineCom(object):
self._sendCommand("M29")
filename = self._currentFile.getFilename()
self._currentFile = None
self._callback.mcFileTransferDone()
self._changeState(self.STATE_OPERATIONAL)
self._callback.mcFileTransferDone(filename)
eventManager().fire("TransferDone", filename)
self.refreshSdFiles()
else: