Refactored server.py into submodules, extracted ajax and rest api into their own blueprints, and while doing all this also took care of #291
This commit is contained in:
parent
1a6030c737
commit
fe37ab3cd8
17 changed files with 1534 additions and 1295 deletions
File diff suppressed because it is too large
Load diff
270
src/octoprint/server/__init__.py
Normal file
270
src/octoprint/server/__init__.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
from sockjs.tornado import SockJSRouter
|
||||
from flask import Flask, render_template, send_from_directory
|
||||
from flask.ext.login import LoginManager
|
||||
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
|
||||
|
||||
import os
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
SUCCESS = {}
|
||||
|
||||
app = Flask("octoprint")
|
||||
debug = False
|
||||
|
||||
printer = None
|
||||
gcodeManager = None
|
||||
userManager = None
|
||||
eventManager = None
|
||||
loginManager = None
|
||||
|
||||
principals = Principal(app)
|
||||
admin_permission = Permission(RoleNeed("admin"))
|
||||
user_permission = Permission(RoleNeed("user"))
|
||||
|
||||
|
||||
from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection
|
||||
from octoprint.printer import Printer, getConnectionOptions
|
||||
from octoprint.settings import settings
|
||||
import octoprint.gcodefiles as gcodefiles
|
||||
import octoprint.util as util
|
||||
import octoprint.users as users
|
||||
import octoprint.events as events
|
||||
import octoprint.timelapse
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
branch = None
|
||||
commit = None
|
||||
try:
|
||||
branch, commit = util.getGitInfo()
|
||||
except:
|
||||
pass
|
||||
|
||||
global debug
|
||||
|
||||
return render_template(
|
||||
"index.jinja2",
|
||||
webcamStream=settings().get(["webcam", "stream"]),
|
||||
enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None),
|
||||
enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]),
|
||||
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
|
||||
enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0,
|
||||
enableAccessControl=userManager is not None,
|
||||
enableSdSupport=settings().get(["feature", "sdSupport"]),
|
||||
firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()),
|
||||
debug=debug,
|
||||
gitBranch=branch,
|
||||
gitCommit=commit
|
||||
)
|
||||
|
||||
|
||||
@app.route("/robots.txt")
|
||||
def robotsTxt():
|
||||
return send_from_directory(app.static_folder, "robots.txt")
|
||||
|
||||
|
||||
@identity_loaded.connect_via(app)
|
||||
def on_identity_loaded(sender, identity):
|
||||
user = load_user(identity.id)
|
||||
if user is None:
|
||||
return
|
||||
|
||||
identity.provides.add(UserNeed(user.get_name()))
|
||||
if user.is_user():
|
||||
identity.provides.add(RoleNeed("user"))
|
||||
if user.is_admin():
|
||||
identity.provides.add(RoleNeed("admin"))
|
||||
|
||||
|
||||
def load_user(id):
|
||||
if userManager is not None:
|
||||
return userManager.findUser(id)
|
||||
return users.DummyUser()
|
||||
|
||||
|
||||
#~~ startup code
|
||||
|
||||
|
||||
class Server():
|
||||
def __init__(self, configfile=None, basedir=None, host="0.0.0.0", port=5000, debug=False, allowRoot=False):
|
||||
self._configfile = configfile
|
||||
self._basedir = basedir
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._debug = debug
|
||||
self._allowRoot = allowRoot
|
||||
|
||||
|
||||
def run(self):
|
||||
if not self._allowRoot:
|
||||
self._checkForRoot()
|
||||
|
||||
global printer
|
||||
global gcodeManager
|
||||
global userManager
|
||||
global eventManager
|
||||
global loginManager
|
||||
global debug
|
||||
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import Application, FallbackHandler
|
||||
|
||||
debug = self._debug
|
||||
|
||||
# first initialize the settings singleton and make sure it uses given configfile and basedir if available
|
||||
self._initSettings(self._configfile, self._basedir)
|
||||
|
||||
# then initialize logging
|
||||
self._initLogging(self._debug)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
eventManager = events.eventManager()
|
||||
gcodeManager = gcodefiles.GcodeManager()
|
||||
printer = Printer(gcodeManager)
|
||||
|
||||
# configure timelapse
|
||||
octoprint.timelapse.configureTimelapse()
|
||||
|
||||
# setup system and gcode command triggers
|
||||
events.SystemCommandTrigger(printer)
|
||||
events.GcodeCommandTrigger(printer)
|
||||
if self._debug:
|
||||
events.DebugEventListener()
|
||||
|
||||
if settings().getBoolean(["accessControl", "enabled"]):
|
||||
userManagerName = settings().get(["accessControl", "userManager"])
|
||||
try:
|
||||
clazz = util.getClass(userManagerName)
|
||||
userManager = clazz()
|
||||
except AttributeError, e:
|
||||
logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName)
|
||||
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
|
||||
app.secret_key = "k3PuVYgtxNm8DXKKTw2nWmFQQun9qceV"
|
||||
loginManager = LoginManager()
|
||||
loginManager.session_protection = "strong"
|
||||
loginManager.user_callback = load_user
|
||||
if userManager is None:
|
||||
loginManager.anonymous_user = users.DummyUser
|
||||
principals.identity_loaders.appendleft(users.dummy_identity_loader)
|
||||
loginManager.init_app(app)
|
||||
|
||||
if self._host is None:
|
||||
self._host = settings().get(["server", "host"])
|
||||
if self._port is None:
|
||||
self._port = settings().getInt(["server", "port"])
|
||||
|
||||
logger.info("Listening on http://%s:%d" % (self._host, self._port))
|
||||
app.debug = self._debug
|
||||
|
||||
from octoprint.server.ajax import ajax
|
||||
from octoprint.server.api import api
|
||||
|
||||
app.register_blueprint(ajax, url_prefix="/ajax")
|
||||
app.register_blueprint(api, url_prefix="/api")
|
||||
|
||||
self._router = SockJSRouter(self._createSocketConnection, "/sockjs")
|
||||
|
||||
self._tornado_app = Application(self._router.urls + [
|
||||
(r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}),
|
||||
(r"/downloads/gcode/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}),
|
||||
(r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)})
|
||||
])
|
||||
self._server = HTTPServer(self._tornado_app)
|
||||
self._server.listen(self._port, address=self._host)
|
||||
|
||||
eventManager.fire("Startup")
|
||||
if settings().getBoolean(["serial", "autoconnect"]):
|
||||
(port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"])
|
||||
connectionOptions = getConnectionOptions()
|
||||
if port in connectionOptions["ports"]:
|
||||
printer.connect(port, baudrate)
|
||||
try:
|
||||
IOLoop.instance().start()
|
||||
except:
|
||||
logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!")
|
||||
logger.exception("Stacktrace follows:")
|
||||
|
||||
def _createSocketConnection(self, session):
|
||||
global printer, gcodeManager, userManager, eventManager
|
||||
return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session)
|
||||
|
||||
def _checkForRoot(self):
|
||||
if "geteuid" in dir(os) and os.geteuid() == 0:
|
||||
exit("You should not run OctoPrint as root!")
|
||||
|
||||
def _initSettings(self, configfile, basedir):
|
||||
s = settings(init=True, basedir=basedir, configfile=configfile)
|
||||
|
||||
def _initLogging(self, debug):
|
||||
config = {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stdout"
|
||||
},
|
||||
"file": {
|
||||
"class": "logging.handlers.TimedRotatingFileHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "simple",
|
||||
"when": "D",
|
||||
"backupCount": "1",
|
||||
"filename": os.path.join(settings().getBaseFolder("logs"), "octoprint.log")
|
||||
},
|
||||
"serialFile": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "simple",
|
||||
"maxBytes": 2 * 1024 * 1024, # let's limit the serial log to 2MB in size
|
||||
"filename": os.path.join(settings().getBaseFolder("logs"), "serial.log")
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
#"octoprint.timelapse": {
|
||||
# "level": "DEBUG"
|
||||
#},
|
||||
#"octoprint.events": {
|
||||
# "level": "DEBUG"
|
||||
#},
|
||||
"SERIAL": {
|
||||
"level": "CRITICAL",
|
||||
"handlers": ["serialFile"],
|
||||
"propagate": False
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console", "file"]
|
||||
}
|
||||
}
|
||||
|
||||
if debug:
|
||||
config["root"]["level"] = "DEBUG"
|
||||
|
||||
logging.config.dictConfig(config)
|
||||
|
||||
if settings().getBoolean(["serial", "log"]):
|
||||
# enable debug logging to serial.log
|
||||
logging.getLogger("SERIAL").setLevel(logging.DEBUG)
|
||||
logging.getLogger("SERIAL").debug("Enabling serial logging")
|
||||
|
||||
if __name__ == "__main__":
|
||||
octoprint = Server()
|
||||
octoprint.run()
|
||||
144
src/octoprint/server/ajax/__init__.py
Normal file
144
src/octoprint/server/ajax/__init__.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import netaddr
|
||||
|
||||
from flask import Blueprint, request, jsonify, abort, current_app, session, make_response
|
||||
from flask.ext.login import login_user, logout_user, current_user
|
||||
from flask.ext.principal import Identity, identity_changed, AnonymousIdentity
|
||||
|
||||
import octoprint.util as util
|
||||
import octoprint.users as users
|
||||
from octoprint.server import restricted_access, SUCCESS, admin_permission, loginManager, principals, userManager
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
|
||||
#~~ init ajax blueprint, including sub modules
|
||||
|
||||
ajax = Blueprint("ajax", __name__)
|
||||
|
||||
import octoprint.server.ajax.control
|
||||
import octoprint.server.ajax.gcodefiles
|
||||
import octoprint.server.ajax.settings
|
||||
import octoprint.server.ajax.timelapse
|
||||
import octoprint.server.ajax.users
|
||||
|
||||
|
||||
#~~ first run setup
|
||||
|
||||
|
||||
@ajax.route("/setup", methods=["POST"])
|
||||
def firstRunSetup():
|
||||
global userManager
|
||||
|
||||
if not settings().getBoolean(["server", "firstRun"]):
|
||||
abort(403)
|
||||
|
||||
if "ac" in request.values.keys() and request.values["ac"] in valid_boolean_trues and \
|
||||
"user" in request.values.keys() and "pass1" in request.values.keys() and \
|
||||
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
|
||||
# configure access control
|
||||
settings().setBoolean(["accessControl", "enabled"], True)
|
||||
userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"])
|
||||
settings().setBoolean(["server", "firstRun"], False)
|
||||
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
|
||||
# disable access control
|
||||
settings().setBoolean(["accessControl", "enabled"], False)
|
||||
settings().setBoolean(["server", "firstRun"], False)
|
||||
|
||||
userManager = None
|
||||
loginManager.anonymous_user = users.DummyUser
|
||||
principals.identity_loaders.appendleft(users.dummy_identity_loader)
|
||||
|
||||
settings().save()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
#~~ system control
|
||||
|
||||
|
||||
@ajax.route("/system", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def performSystemAction():
|
||||
logger = logging.getLogger(__name__)
|
||||
if request.values.has_key("action"):
|
||||
action = request.values["action"]
|
||||
availableActions = settings().get(["system", "actions"])
|
||||
for availableAction in availableActions:
|
||||
if availableAction["action"] == action:
|
||||
logger.info("Performing command: %s" % availableAction["command"])
|
||||
try:
|
||||
subprocess.check_output(availableAction["command"], shell=True)
|
||||
except subprocess.CalledProcessError, e:
|
||||
logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message))
|
||||
return make_response(("Command failed with return code %i: %s" % (e.returncode, e.message), 500, []))
|
||||
except Exception, ex:
|
||||
logger.exception("Command failed")
|
||||
return make_response(("Command failed: %r" % ex, 500, []))
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
#~~ Login/user handling
|
||||
|
||||
|
||||
@ajax.route("/login", methods=["POST"])
|
||||
def login():
|
||||
if userManager is not None and "user" in request.values.keys() and "pass" in request.values.keys():
|
||||
username = request.values["user"]
|
||||
password = request.values["pass"]
|
||||
|
||||
if "remember" in request.values.keys() and request.values["remember"] == "true":
|
||||
remember = True
|
||||
else:
|
||||
remember = False
|
||||
|
||||
user = userManager.findUser(username)
|
||||
if user is not None:
|
||||
if user.check_password(users.UserManager.createPasswordHash(password)):
|
||||
login_user(user, remember=remember)
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
||||
return jsonify(user.asDict())
|
||||
return make_response(("User unknown or password incorrect", 401, []))
|
||||
elif "passive" in request.values.keys():
|
||||
user = current_user
|
||||
if user is not None and not user.is_anonymous():
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
||||
return jsonify(user.asDict())
|
||||
elif settings().getBoolean(["accessControl", "autologinLocal"]) \
|
||||
and settings().get(["accessControl", "autologinAs"]) is not None \
|
||||
and settings().get(["accessControl", "localNetworks"]) is not None:
|
||||
|
||||
autologinAs = settings().get(["accessControl", "autologinAs"])
|
||||
localNetworks = netaddr.IPSet([])
|
||||
for ip in settings().get(["accessControl", "localNetworks"]):
|
||||
localNetworks.add(ip)
|
||||
|
||||
try:
|
||||
remoteAddr = util.getRemoteAddress(request)
|
||||
if netaddr.IPAddress(remoteAddr) in localNetworks:
|
||||
user = userManager.findUser(autologinAs)
|
||||
if user is not None:
|
||||
login_user(user)
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id()))
|
||||
return jsonify(user.asDict())
|
||||
except:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks))
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
@ajax.route("/logout", methods=["POST"])
|
||||
@restricted_access
|
||||
def logout():
|
||||
# Remove session keys set by Flask-Principal
|
||||
for key in ('identity.id', 'identity.auth_type'):
|
||||
del session[key]
|
||||
identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())
|
||||
|
||||
logout_user()
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
175
src/octoprint/server/ajax/control.py
Normal file
175
src/octoprint/server/ajax/control.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# coding=utf-8
|
||||
__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 octoprint.settings import settings
|
||||
from octoprint.printer import getConnectionOptions
|
||||
|
||||
from octoprint.server import printer, restricted_access, SUCCESS
|
||||
from octoprint.server.ajax import ajax
|
||||
|
||||
|
||||
#~~ Printer control
|
||||
|
||||
|
||||
@ajax.route("/control/connection/options", methods=["GET"])
|
||||
def connectionOptions():
|
||||
return jsonify(getConnectionOptions())
|
||||
|
||||
|
||||
@ajax.route("/control/connection", methods=["POST"])
|
||||
@restricted_access
|
||||
def connect():
|
||||
if "command" in request.values.keys() and request.values["command"] == "connect":
|
||||
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():
|
||||
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()
|
||||
printer.connect(port=port, baudrate=baudrate)
|
||||
elif "command" in request.values.keys() and request.values["command"] == "disconnect":
|
||||
printer.disconnect()
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
@ajax.route("/control/command", methods=["POST"])
|
||||
@restricted_access
|
||||
def printerCommand():
|
||||
if "application/json" in request.headers["Content-Type"]:
|
||||
data = request.json
|
||||
|
||||
parameters = {}
|
||||
if "parameters" in data.keys(): parameters = data["parameters"]
|
||||
|
||||
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()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
@ajax.route("/control/temperature", methods=["POST"])
|
||||
@restricted_access
|
||||
def setTargetTemperature():
|
||||
if "temp" in request.values.keys():
|
||||
# set target temperature
|
||||
temp = request.values["temp"]
|
||||
printer.command("M104 S" + temp)
|
||||
|
||||
if "bedTemp" in request.values.keys():
|
||||
# set target bed temperature
|
||||
bedTemp = request.values["bedTemp"]
|
||||
printer.command("M140 S" + bedTemp)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
@ajax.route("/control/jog", methods=["POST"])
|
||||
@restricted_access
|
||||
def jog():
|
||||
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)
|
||||
|
||||
(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"])
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
@ajax.route("/control/custom", methods=["GET"])
|
||||
def getCustomControls():
|
||||
customControls = settings().get(["controls"])
|
||||
return jsonify(controls=customControls)
|
||||
|
||||
|
||||
@ajax.route("/control/sd", methods=["POST"])
|
||||
@restricted_access
|
||||
def sdCommand():
|
||||
if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting():
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
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()
|
||||
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
135
src/octoprint/server/ajax/gcodefiles.py
Normal file
135
src/octoprint/server/ajax/gcodefiles.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# coding=utf-8
|
||||
__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, make_response, url_for
|
||||
|
||||
import octoprint.gcodefiles as gcodefiles
|
||||
import octoprint.util as util
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
|
||||
from octoprint.server import printer, gcodeManager, restricted_access, SUCCESS
|
||||
from octoprint.server.util import redirectToTornado
|
||||
from octoprint.server.ajax import ajax
|
||||
|
||||
|
||||
#~~ GCODE file handling
|
||||
|
||||
|
||||
@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"
|
||||
})
|
||||
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/upload", methods=["POST"])
|
||||
@restricted_access
|
||||
def uploadGcodeFile():
|
||||
if "gcode_file" in request.files.keys():
|
||||
file = request.files["gcode_file"]
|
||||
sd = "target" in request.values.keys() and request.values["target"] == "sd";
|
||||
|
||||
currentFilename = None
|
||||
currentSd = None
|
||||
currentJob = printer.getCurrentJob()
|
||||
if currentJob is not None and "filename" in currentJob.keys() and "sd" in currentJob.keys():
|
||||
currentFilename = currentJob["filename"]
|
||||
currentSd = currentJob["sd"]
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
destination = FileDestinations.SDCARD if sd else FileDestinations.LOCAL
|
||||
|
||||
filename, done = gcodeManager.addFile(file, destination)
|
||||
|
||||
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)
|
||||
|
||||
global eventManager
|
||||
eventManager.fire("Upload", filename)
|
||||
return jsonify(files=gcodeManager.getAllFileData(), filename=filename, done=done)
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles/load", methods=["POST"])
|
||||
@restricted_access
|
||||
def loadGcodeFile():
|
||||
if "filename" in request.values.keys():
|
||||
printAfterLoading = False
|
||||
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
|
||||
printAfterLoading = True
|
||||
|
||||
sd = False
|
||||
if "target" in request.values.keys() and request.values["target"] == "sd":
|
||||
filename = request.values["filename"]
|
||||
sd = True
|
||||
else:
|
||||
filename = gcodeManager.getAbsolutePath(request.values["filename"])
|
||||
printer.selectFile(filename, sd, printAfterLoading)
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
|
||||
@ajax.route("/gcodefiles/delete", methods=["POST"])
|
||||
@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"
|
||||
|
||||
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 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"])
|
||||
@restricted_access
|
||||
def refreshFiles():
|
||||
printer.updateSdFiles()
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
185
src/octoprint/server/ajax/settings.py
Normal file
185
src/octoprint/server/ajax/settings.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import logging
|
||||
|
||||
from flask import request, jsonify
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.printer import getConnectionOptions
|
||||
|
||||
from octoprint.server import restricted_access, admin_permission
|
||||
from octoprint.server.ajax import ajax
|
||||
|
||||
|
||||
#~~ settings
|
||||
|
||||
|
||||
@ajax.route("/settings", methods=["GET"])
|
||||
def getSettings():
|
||||
s = settings()
|
||||
|
||||
[movementSpeedX, movementSpeedY, movementSpeedZ, movementSpeedE] = s.get(["printerParameters", "movementSpeed", ["x", "y", "z", "e"]])
|
||||
|
||||
connectionOptions = getConnectionOptions()
|
||||
|
||||
return jsonify({
|
||||
"api": {
|
||||
"enabled": s.getBoolean(["api", "enabled"]),
|
||||
"key": s.get(["api", "key"])
|
||||
},
|
||||
"appearance": {
|
||||
"name": s.get(["appearance", "name"]),
|
||||
"color": s.get(["appearance", "color"])
|
||||
},
|
||||
"printer": {
|
||||
"movementSpeedX": movementSpeedX,
|
||||
"movementSpeedY": movementSpeedY,
|
||||
"movementSpeedZ": movementSpeedZ,
|
||||
"movementSpeedE": movementSpeedE,
|
||||
"invertAxes": s.get(["printerParameters", "invertAxes"])
|
||||
},
|
||||
"webcam": {
|
||||
"streamUrl": s.get(["webcam", "stream"]),
|
||||
"snapshotUrl": s.get(["webcam", "snapshot"]),
|
||||
"ffmpegPath": s.get(["webcam", "ffmpeg"]),
|
||||
"bitrate": s.get(["webcam", "bitrate"]),
|
||||
"watermark": s.getBoolean(["webcam", "watermark"]),
|
||||
"flipH": s.getBoolean(["webcam", "flipH"]),
|
||||
"flipV": s.getBoolean(["webcam", "flipV"])
|
||||
},
|
||||
"feature": {
|
||||
"gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]),
|
||||
"temperatureGraph": s.getBoolean(["feature", "temperatureGraph"]),
|
||||
"waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]),
|
||||
"alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]),
|
||||
"sdSupport": s.getBoolean(["feature", "sdSupport"]),
|
||||
"swallowOkAfterResend": s.getBoolean(["feature", "swallowOkAfterResend"])
|
||||
},
|
||||
"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"]),
|
||||
"log": s.getBoolean(["serial", "log"])
|
||||
},
|
||||
"folder": {
|
||||
"uploads": s.getBaseFolder("uploads"),
|
||||
"timelapse": s.getBaseFolder("timelapse"),
|
||||
"timelapseTmp": s.getBaseFolder("timelapse_tmp"),
|
||||
"logs": s.getBaseFolder("logs")
|
||||
},
|
||||
"temperature": {
|
||||
"profiles": s.get(["temperature", "profiles"])
|
||||
},
|
||||
"system": {
|
||||
"actions": s.get(["system", "actions"]),
|
||||
"events": s.get(["system", "events"])
|
||||
},
|
||||
"terminalFilters": s.get(["terminalFilters"]),
|
||||
"cura": {
|
||||
"enabled": s.getBoolean(["cura", "enabled"]),
|
||||
"path": s.get(["cura", "path"]),
|
||||
"config": s.get(["cura", "config"])
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ajax.route("/settings", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def setSettings():
|
||||
if "application/json" in request.headers["Content-Type"]:
|
||||
data = request.json
|
||||
s = settings()
|
||||
|
||||
if "api" in data.keys():
|
||||
if "enabled" in data["api"].keys(): s.set(["api", "enabled"], data["api"]["enabled"])
|
||||
if "key" in data["api"].keys(): s.set(["api", "key"], data["api"]["key"], True)
|
||||
|
||||
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 "printer" in data.keys():
|
||||
if "movementSpeedX" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "x"], data["printer"]["movementSpeedX"])
|
||||
if "movementSpeedY" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "y"], data["printer"]["movementSpeedY"])
|
||||
if "movementSpeedZ" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "z"], data["printer"]["movementSpeedZ"])
|
||||
if "movementSpeedE" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "e"], data["printer"]["movementSpeedE"])
|
||||
if "invertAxes" in data["printer"].keys(): s.set(["printerParameters", "invertAxes"], data["printer"]["invertAxes"])
|
||||
|
||||
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 "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 "feature" in data.keys():
|
||||
if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["feature", "gCodeVisualizer"], data["feature"]["gcodeViewer"])
|
||||
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 "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"])
|
||||
if "swallowOkAfterResend" in data["feature"].keys(): s.setBoolean(["feature", "swallowOkAfterResend"], data["feature"]["swallowOkAfterResend"])
|
||||
|
||||
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"])
|
||||
|
||||
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 "temperature" in data.keys():
|
||||
if "profiles" in data["temperature"].keys(): s.set(["temperature", "profiles"], data["temperature"]["profiles"])
|
||||
|
||||
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"])
|
||||
|
||||
cura = data.get("cura", None)
|
||||
if cura:
|
||||
path = cura.get("path")
|
||||
if path:
|
||||
s.set(["cura", "path"], path)
|
||||
|
||||
config = cura.get("config")
|
||||
if config:
|
||||
s.set(["cura", "config"], config)
|
||||
|
||||
# Enabled is a boolean so we cannot check that we have a result
|
||||
enabled = cura.get("enabled")
|
||||
s.setBoolean(["cura", "enabled"], enabled)
|
||||
|
||||
s.save()
|
||||
|
||||
return getSettings()
|
||||
|
||||
88
src/octoprint/server/ajax/timelapse.py
Normal file
88
src/octoprint/server/ajax/timelapse.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import os
|
||||
|
||||
from flask import request, jsonify, url_for
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import octoprint.timelapse
|
||||
import octoprint.util as util
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
|
||||
from octoprint.server import restricted_access, admin_permission
|
||||
from octoprint.server.util import redirectToTornado
|
||||
from octoprint.server.ajax import ajax
|
||||
|
||||
|
||||
#~~ timelapse handling
|
||||
|
||||
|
||||
@ajax.route("/timelapse", methods=["GET"])
|
||||
def getTimelapseData():
|
||||
timelapse = octoprint.timelapse.current
|
||||
|
||||
type = "off"
|
||||
additionalConfig = {}
|
||||
if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
|
||||
type = "zchange"
|
||||
elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
|
||||
type = "timed"
|
||||
additionalConfig = {
|
||||
"interval": timelapse.interval()
|
||||
}
|
||||
|
||||
files = octoprint.timelapse.getFinishedTimelapses()
|
||||
for file in files:
|
||||
file["url"] = url_for("index") + "downloads/timelapse/" + file["name"]
|
||||
|
||||
return jsonify({
|
||||
"type": type,
|
||||
"config": additionalConfig,
|
||||
"files": files
|
||||
})
|
||||
|
||||
|
||||
@ajax.route("/timelapse/<filename>", methods=["GET"])
|
||||
def downloadTimelapse(filename):
|
||||
return redirectToTornado(request, url_for("index") + "downloads/timelapse/" + filename)
|
||||
|
||||
|
||||
@ajax.route("/timelapse/<filename>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def deleteTimelapse(filename):
|
||||
if util.isAllowedFile(filename, {"mpg"}):
|
||||
secure = os.path.join(settings().getBaseFolder("timelapse"), secure_filename(filename))
|
||||
if os.path.exists(secure):
|
||||
os.remove(secure)
|
||||
return getTimelapseData()
|
||||
|
||||
|
||||
@ajax.route("/timelapse", methods=["POST"])
|
||||
@restricted_access
|
||||
def setTimelapseConfig():
|
||||
if "type" in request.values:
|
||||
config = {
|
||||
"type": request.values["type"],
|
||||
"options": {}
|
||||
}
|
||||
|
||||
if "interval" in request.values:
|
||||
interval = 10
|
||||
try:
|
||||
interval = int(request.values["interval"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
config["options"] = {
|
||||
"interval": interval
|
||||
}
|
||||
|
||||
if admin_permission.can() and "save" in request.values and request.values["save"] in valid_boolean_trues:
|
||||
octoprint.timelapse.configureTimelapse(config, True)
|
||||
else:
|
||||
octoprint.timelapse.configureTimelapse(config)
|
||||
|
||||
return getTimelapseData()
|
||||
|
||||
125
src/octoprint/server/ajax/users.py
Normal file
125
src/octoprint/server/ajax/users.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# coding=utf-8
|
||||
__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, abort, make_response
|
||||
from flask.ext.login import current_user
|
||||
|
||||
import octoprint.users as users
|
||||
|
||||
from octoprint.server import restricted_access, SUCCESS, admin_permission, userManager
|
||||
from octoprint.server.ajax import ajax
|
||||
|
||||
|
||||
#~~ user settings
|
||||
|
||||
|
||||
@ajax.route("/users", methods=["GET"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def getUsers():
|
||||
if userManager is None:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
return jsonify({"users": userManager.getAllUsers()})
|
||||
|
||||
|
||||
@ajax.route("/users", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def addUser():
|
||||
if userManager is None:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
if "application/json" in request.headers["Content-Type"]:
|
||||
data = request.json
|
||||
|
||||
name = data["name"]
|
||||
password = data["password"]
|
||||
active = data["active"]
|
||||
|
||||
roles = ["user"]
|
||||
if "admin" in data.keys() and data["admin"]:
|
||||
roles.append("admin")
|
||||
|
||||
try:
|
||||
userManager.addUser(name, password, active, roles)
|
||||
except users.UserAlreadyExists:
|
||||
abort(409)
|
||||
return getUsers()
|
||||
|
||||
|
||||
@ajax.route("/users/<username>", methods=["GET"])
|
||||
@restricted_access
|
||||
def getUser(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()):
|
||||
user = userManager.findUser(username)
|
||||
if user is not None:
|
||||
return jsonify(user.asDict())
|
||||
else:
|
||||
abort(404)
|
||||
else:
|
||||
abort(403)
|
||||
|
||||
|
||||
@ajax.route("/users/<username>", methods=["PUT"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def updateUser(username):
|
||||
if userManager is None:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
user = userManager.findUser(username)
|
||||
if user is not None:
|
||||
if "application/json" in request.headers["Content-Type"]:
|
||||
data = request.json
|
||||
|
||||
# change roles
|
||||
roles = ["user"]
|
||||
if "admin" in data.keys() and data["admin"]:
|
||||
roles.append("admin")
|
||||
userManager.changeUserRoles(username, roles)
|
||||
|
||||
# change activation
|
||||
if "active" in data.keys():
|
||||
userManager.changeUserActivation(username, data["active"])
|
||||
return getUsers()
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
||||
@ajax.route("/users/<username>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
@admin_permission.require(http_exception=403)
|
||||
def removeUser(username):
|
||||
if userManager is None:
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
try:
|
||||
userManager.removeUser(username)
|
||||
return getUsers()
|
||||
except users.UnknownUser:
|
||||
abort(404)
|
||||
|
||||
|
||||
@ajax.route("/users/<username>/password", methods=["PUT"])
|
||||
@restricted_access
|
||||
def changePasswordForUser(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()):
|
||||
if "application/json" in request.headers["Content-Type"]:
|
||||
data = request.json
|
||||
if "password" in data.keys() and data["password"]:
|
||||
try:
|
||||
userManager.changeUserPassword(username, data["password"])
|
||||
except users.UnknownUser:
|
||||
return make_response(("Unknown user: %s" % username, 404, []))
|
||||
return jsonify(SUCCESS)
|
||||
else:
|
||||
return make_response(("Forbidden", 403, []))
|
||||
|
||||
67
src/octoprint/server/api.py
Normal file
67
src/octoprint/server/api.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, request, jsonify, abort
|
||||
|
||||
from octoprint.server import printer, gcodeManager, SUCCESS
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
import octoprint.gcodefiles as gcodefiles
|
||||
|
||||
api = Blueprint("api", __name__)
|
||||
|
||||
#-- very simple api routines
|
||||
@api.route("/load", methods=["POST"])
|
||||
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):
|
||||
abort(400)
|
||||
|
||||
filename, done = gcodeManager.addFile(file)
|
||||
if filename is None:
|
||||
logger.warn("Upload via API failed")
|
||||
abort(500)
|
||||
|
||||
# Immediately perform a file select and possibly print too
|
||||
printAfterSelect = False
|
||||
if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues:
|
||||
printAfterSelect = True
|
||||
filepath = gcodeManager.getAbsolutePath(filename)
|
||||
if filepath is not None:
|
||||
printer.selectFile(filepath, False, printAfterSelect)
|
||||
return jsonify(SUCCESS)
|
||||
|
||||
@api.route("/state", methods=["GET"])
|
||||
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()
|
||||
})
|
||||
return jsonify(currentData)
|
||||
|
||||
296
src/octoprint/server/util.py
Normal file
296
src/octoprint/server/util.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
|
||||
from tornado.web import StaticFileHandler, HTTPError
|
||||
from flask import url_for, make_response
|
||||
from flask.ext.login import login_required
|
||||
from werkzeug.utils import redirect
|
||||
from sockjs.tornado import SockJSConnection
|
||||
|
||||
import datetime
|
||||
import stat
|
||||
import mimetypes
|
||||
import email
|
||||
import time
|
||||
import os
|
||||
import threading
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from octoprint.server import userManager
|
||||
from octoprint.settings import settings
|
||||
import octoprint.timelapse
|
||||
|
||||
|
||||
def restricted_access(func):
|
||||
"""
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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()):
|
||||
return make_response("OctoPrint isn't setup yet", 403)
|
||||
return login_required(func)(*args, **kwargs)
|
||||
return decorated_view
|
||||
|
||||
|
||||
#~~ Printer state
|
||||
|
||||
|
||||
class PrinterStateConnection(SockJSConnection):
|
||||
def __init__(self, printer, gcodeManager, userManager, eventManager, session):
|
||||
SockJSConnection.__init__(self, session)
|
||||
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
self._temperatureBacklog = []
|
||||
self._temperatureBacklogMutex = threading.Lock()
|
||||
self._logBacklog = []
|
||||
self._logBacklogMutex = threading.Lock()
|
||||
self._messageBacklog = []
|
||||
self._messageBacklogMutex = threading.Lock()
|
||||
|
||||
self._printer = printer
|
||||
self._gcodeManager = gcodeManager
|
||||
self._userManager = userManager
|
||||
self._eventManager = eventManager
|
||||
|
||||
def _getRemoteAddress(self, info):
|
||||
forwardedFor = info.headers.get("X-Forwarded-For")
|
||||
if forwardedFor is not None:
|
||||
return forwardedFor.split(",")[0]
|
||||
return info.ip
|
||||
|
||||
def on_open(self, info):
|
||||
self._logger.info("New connection from client: %s" % self._getRemoteAddress(info))
|
||||
self._printer.registerCallback(self)
|
||||
self._gcodeManager.registerCallback(self)
|
||||
octoprint.timelapse.registerCallback(self)
|
||||
|
||||
self._eventManager.fire("ClientOpened")
|
||||
self._eventManager.subscribe("MovieDone", self._onMovieDone)
|
||||
self._eventManager.subscribe("SlicingStarted", self._onSlicingStarted)
|
||||
self._eventManager.subscribe("SlicingDone", self._onSlicingDone)
|
||||
self._eventManager.subscribe("SlicingFailed", self._onSlicingFailed)
|
||||
|
||||
octoprint.timelapse.notifyCallbacks(octoprint.timelapse.current)
|
||||
|
||||
def on_close(self):
|
||||
self._logger.info("Closed client connection")
|
||||
self._printer.unregisterCallback(self)
|
||||
self._gcodeManager.unregisterCallback(self)
|
||||
octoprint.timelapse.unregisterCallback(self)
|
||||
|
||||
self._eventManager.fire("ClientClosed")
|
||||
self._eventManager.unsubscribe("MovieDone", self._onMovieDone)
|
||||
self._eventManager.unsubscribe("SlicingStarted", self._onSlicingStarted)
|
||||
self._eventManager.unsubscribe("SlicingDone", self._onSlicingDone)
|
||||
self._eventManager.unsubscribe("SlicingFailed", self._onSlicingFailed)
|
||||
|
||||
def on_message(self, message):
|
||||
pass
|
||||
|
||||
def sendCurrentData(self, data):
|
||||
# add current temperature, log and message backlogs to sent data
|
||||
with self._temperatureBacklogMutex:
|
||||
temperatures = self._temperatureBacklog
|
||||
self._temperatureBacklog = []
|
||||
|
||||
with self._logBacklogMutex:
|
||||
logs = self._logBacklog
|
||||
self._logBacklog = []
|
||||
|
||||
with self._messageBacklogMutex:
|
||||
messages = self._messageBacklog
|
||||
self._messageBacklog = []
|
||||
|
||||
data.update({
|
||||
"temperatures": temperatures,
|
||||
"logs": logs,
|
||||
"messages": messages
|
||||
})
|
||||
self._emit("current", data)
|
||||
|
||||
def sendHistoryData(self, data):
|
||||
self._emit("history", data)
|
||||
|
||||
def sendUpdateTrigger(self, type, payload=None):
|
||||
self._emit("updateTrigger", {"type": type, "payload": payload})
|
||||
|
||||
def sendFeedbackCommandOutput(self, name, output):
|
||||
self._emit("feedbackCommandOutput", {"name": name, "output": output})
|
||||
|
||||
def sendTimelapseConfig(self, timelapseConfig):
|
||||
self._emit("timelapse", timelapseConfig)
|
||||
|
||||
def addLog(self, data):
|
||||
with self._logBacklogMutex:
|
||||
self._logBacklog.append(data)
|
||||
|
||||
def addMessage(self, data):
|
||||
with self._messageBacklogMutex:
|
||||
self._messageBacklog.append(data)
|
||||
|
||||
def addTemperature(self, data):
|
||||
with self._temperatureBacklogMutex:
|
||||
self._temperatureBacklog.append(data)
|
||||
|
||||
def _onMovieDone(self, event, payload):
|
||||
self.sendUpdateTrigger("timelapseFiles")
|
||||
|
||||
def _onSlicingStarted(self, event, payload):
|
||||
self.sendUpdateTrigger("slicingStarted", payload)
|
||||
|
||||
def _onSlicingDone(self, event, payload):
|
||||
self.sendUpdateTrigger("slicingDone", payload)
|
||||
|
||||
def _onSlicingFailed(self, event, payload):
|
||||
self.sendUpdateTrigger("slicingFailed", payload)
|
||||
|
||||
def _emit(self, type, payload):
|
||||
self.send({type: payload})
|
||||
|
||||
|
||||
#~~ customized large response handler
|
||||
|
||||
|
||||
class LargeResponseHandler(StaticFileHandler):
|
||||
|
||||
CHUNK_SIZE = 16 * 1024
|
||||
|
||||
def initialize(self, path, default_filename=None, as_attachment=False):
|
||||
StaticFileHandler.initialize(self, path, default_filename)
|
||||
self._as_attachment = as_attachment
|
||||
|
||||
def get(self, path, include_body=True):
|
||||
path = self.parse_url_path(path)
|
||||
abspath = os.path.abspath(os.path.join(self.root, path))
|
||||
# os.path.abspath strips a trailing /
|
||||
# it needs to be temporarily added back for requests to root/
|
||||
if not (abspath + os.path.sep).startswith(self.root):
|
||||
raise HTTPError(403, "%s is not in root static directory", path)
|
||||
if os.path.isdir(abspath) and self.default_filename is not None:
|
||||
# need to look at the request.path here for when path is empty
|
||||
# but there is some prefix to the path that was already
|
||||
# trimmed by the routing
|
||||
if not self.request.path.endswith("/"):
|
||||
self.redirect(self.request.path + "/")
|
||||
return
|
||||
abspath = os.path.join(abspath, self.default_filename)
|
||||
if not os.path.exists(abspath):
|
||||
raise HTTPError(404)
|
||||
if not os.path.isfile(abspath):
|
||||
raise HTTPError(403, "%s is not a file", path)
|
||||
|
||||
stat_result = os.stat(abspath)
|
||||
modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
|
||||
|
||||
self.set_header("Last-Modified", modified)
|
||||
|
||||
mime_type, encoding = mimetypes.guess_type(abspath)
|
||||
if mime_type:
|
||||
self.set_header("Content-Type", mime_type)
|
||||
|
||||
cache_time = self.get_cache_time(path, modified, mime_type)
|
||||
|
||||
if cache_time > 0:
|
||||
self.set_header("Expires", datetime.datetime.utcnow() +
|
||||
datetime.timedelta(seconds=cache_time))
|
||||
self.set_header("Cache-Control", "max-age=" + str(cache_time))
|
||||
|
||||
self.set_extra_headers(path)
|
||||
|
||||
# Check the If-Modified-Since, and don't send the result if the
|
||||
# content has not been modified
|
||||
ims_value = self.request.headers.get("If-Modified-Since")
|
||||
if ims_value is not None:
|
||||
date_tuple = email.utils.parsedate(ims_value)
|
||||
if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
|
||||
if if_since >= modified:
|
||||
self.set_status(304)
|
||||
return
|
||||
|
||||
if not include_body:
|
||||
assert self.request.method == "HEAD"
|
||||
self.set_header("Content-Length", stat_result[stat.ST_SIZE])
|
||||
else:
|
||||
with open(abspath, "rb") as file:
|
||||
while True:
|
||||
data = file.read(LargeResponseHandler.CHUNK_SIZE)
|
||||
if not data:
|
||||
break
|
||||
self.write(data)
|
||||
self.flush()
|
||||
|
||||
def set_extra_headers(self, path):
|
||||
if self._as_attachment:
|
||||
self.set_header("Content-Disposition", "attachment")
|
||||
|
||||
|
||||
#~~ reverse proxy compatible wsgi middleware
|
||||
|
||||
|
||||
class ReverseProxied(object):
|
||||
"""
|
||||
Wrap the application in this middleware and configure the
|
||||
front-end server to add these headers, to let you quietly bind
|
||||
this to a URL other than / and to an HTTP scheme that is
|
||||
different than what is used locally.
|
||||
|
||||
In nginx:
|
||||
location /myprefix {
|
||||
proxy_pass http://192.168.0.1:5001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_set_header X-Script-Name /myprefix;
|
||||
}
|
||||
|
||||
Alternatively define prefix and scheme via config.yaml:
|
||||
server:
|
||||
baseUrl: /myprefix
|
||||
scheme: http
|
||||
|
||||
:param app: the WSGI application
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
||||
if not script_name:
|
||||
script_name = settings().get(["server", "baseUrl"])
|
||||
|
||||
if script_name:
|
||||
environ['SCRIPT_NAME'] = script_name
|
||||
path_info = environ['PATH_INFO']
|
||||
if path_info.startswith(script_name):
|
||||
environ['PATH_INFO'] = path_info[len(script_name):]
|
||||
|
||||
scheme = environ.get('HTTP_X_SCHEME', '')
|
||||
if not scheme:
|
||||
scheme = settings().get(["server", "scheme"])
|
||||
|
||||
if scheme:
|
||||
environ['wsgi.url_scheme'] = scheme
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
||||
def redirectToTornado(request, target):
|
||||
requestUrl = request.url
|
||||
appBaseUrl = requestUrl[:requestUrl.find(url_for("ajax.base"))]
|
||||
|
||||
redirectUrl = appBaseUrl + target
|
||||
if "?" in requestUrl:
|
||||
fragment = requestUrl[requestUrl.rfind("?"):]
|
||||
redirectUrl += fragment
|
||||
return redirect(redirectUrl)
|
||||
|
||||
|
|
@ -38,7 +38,9 @@ default_settings = {
|
|||
"server": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"firstRun": True
|
||||
"firstRun": True,
|
||||
"baseUrl": "",
|
||||
"scheme": ""
|
||||
},
|
||||
"webcam": {
|
||||
"stream": None,
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ table {
|
|||
#temperature-graph {
|
||||
height: 350px;
|
||||
width: 100%;
|
||||
background-image: url("/static/img/graph-background.png");
|
||||
background-image: url("../img/graph-background.png");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function GcodeViewModel(loginStateViewModel) {
|
|||
if (self.status == 'idle' && self.errorCount < 3) {
|
||||
self.status = 'request';
|
||||
$.ajax({
|
||||
url: "/downloads/gcode/" + filename,
|
||||
url: BASEURL + "downloads/gcode/" + filename,
|
||||
data: { "mtime": mtime },
|
||||
type: "GET",
|
||||
success: function(response, rstatus) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
<link href="{{ url_for('static', filename='gcodeviewer/css/style.css') }}" rel="stylesheet" media="screen">
|
||||
|
||||
<script lang="javascript">
|
||||
var AJAX_BASEURL = "{{ ajaxBaseUrl }}";
|
||||
var BASEURL = "{{ url_for('index') }}";
|
||||
var AJAX_BASEURL = BASEURL + "ajax/";
|
||||
|
||||
var CONFIG_GCODEFILESPERPAGE = 5;
|
||||
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ import octoprint.util as util
|
|||
from octoprint.settings import settings
|
||||
from octoprint.events import eventManager
|
||||
|
||||
# currently configured timelapse
|
||||
current = None
|
||||
|
||||
|
||||
def getFinishedTimelapses():
|
||||
files = []
|
||||
basedir = settings().getBaseFolder("timelapse")
|
||||
|
|
@ -36,23 +40,55 @@ def getFinishedTimelapses():
|
|||
validTimelapseTypes = ["off", "timed", "zchange"]
|
||||
|
||||
updateCallbacks = []
|
||||
|
||||
|
||||
def registerCallback(callback):
|
||||
if not callback in updateCallbacks:
|
||||
updateCallbacks.append(callback)
|
||||
|
||||
|
||||
def unregisterCallback(callback):
|
||||
if callback in updateCallbacks:
|
||||
updateCallbacks.remove(callback)
|
||||
|
||||
|
||||
def notifyCallbacks(timelapse):
|
||||
if timelapse is None:
|
||||
config = None
|
||||
else:
|
||||
config = timelapse.configData()
|
||||
for callback in updateCallbacks:
|
||||
if timelapse is None:
|
||||
config = None
|
||||
else:
|
||||
config = timelapse.configData()
|
||||
try: callback.sendTimelapseConfig(config)
|
||||
except: pass
|
||||
|
||||
|
||||
def configureTimelapse(config=None, persist=False):
|
||||
global current
|
||||
|
||||
if config is None:
|
||||
config = settings().get(["webcam", "timelapse"])
|
||||
|
||||
if current is not None:
|
||||
current.unload()
|
||||
|
||||
type = config["type"]
|
||||
if type is None or "off" == type:
|
||||
current = None
|
||||
elif "zchange" == type:
|
||||
current = ZTimelapse()
|
||||
elif "timed" == type:
|
||||
interval = 10
|
||||
if "options" in config and "interval" in config["options"]:
|
||||
interval = config["options"]["interval"]
|
||||
current = TimedTimelapse(interval)
|
||||
|
||||
notifyCallbacks(current)
|
||||
|
||||
if persist:
|
||||
settings().set(["webcam", "timelapse"], config)
|
||||
settings().save()
|
||||
|
||||
|
||||
class Timelapse(object):
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
|
@ -232,6 +268,7 @@ class Timelapse(object):
|
|||
continue
|
||||
os.remove(os.path.join(self._captureDir, filename))
|
||||
|
||||
|
||||
class ZTimelapse(Timelapse):
|
||||
def __init__(self):
|
||||
Timelapse.__init__(self)
|
||||
|
|
@ -250,6 +287,7 @@ class ZTimelapse(Timelapse):
|
|||
def _onZChange(self, event, payload):
|
||||
self.captureImage()
|
||||
|
||||
|
||||
class TimedTimelapse(Timelapse):
|
||||
def __init__(self, interval=1):
|
||||
Timelapse.__init__(self)
|
||||
|
|
|
|||
Loading…
Reference in a new issue