Added API documentation, adjusted API and returned data model slightly to be consistent with other parts of the API, made log file management restricted to admins.
TODO: Make downloading of logs also restricted to admins
This commit is contained in:
parent
ad7243844e
commit
ace102e1c1
7 changed files with 253 additions and 27 deletions
|
|
@ -12,4 +12,4 @@ API Documentation
|
|||
connection.rst
|
||||
printer.rst
|
||||
job.rst
|
||||
|
||||
logs.rst
|
||||
|
|
|
|||
177
docs/api/logs.rst
Normal file
177
docs/api/logs.rst
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
.. _sec-api-logs:
|
||||
|
||||
*******************
|
||||
Log file management
|
||||
*******************
|
||||
|
||||
.. note::
|
||||
|
||||
All log file management operations require admin rights.
|
||||
|
||||
.. contents::
|
||||
|
||||
.. _sec-api-logs-list:
|
||||
|
||||
Retrieve a list of available log files
|
||||
======================================
|
||||
|
||||
.. http:post:: /api/logs
|
||||
|
||||
Retrieve information regarding all log files currently available and regarding the disk space still available
|
||||
in the system on the location the log files are being stored.
|
||||
|
||||
Returns a :ref:`Logfile Retrieve response <sec-api-logs-datamodel-retrieveresponse>`.
|
||||
|
||||
**Example Request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/logs HTTP/1.1
|
||||
Host: example.com
|
||||
X-Api-Key: abcdef...
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"files" : [
|
||||
{
|
||||
"date" : 1393158814,
|
||||
"name" : "octoprint.log",
|
||||
"size" : 43712,
|
||||
"refs": {
|
||||
"resource": "http://example.com/api/logs/octoprint.log",
|
||||
"download": "http://example.com/downloads/logs/octoprint.log"
|
||||
}
|
||||
},
|
||||
{
|
||||
"date" : 1392628936,
|
||||
"name" : "octoprint.log.2014-02-17",
|
||||
"size" : 13205,
|
||||
"refs": {
|
||||
"resource": "http://example.com/api/logs/octoprint.log.2014-02-17",
|
||||
"download": "http://example.com/downloads/logs/octoprint.log.2014-02-17"
|
||||
}
|
||||
},
|
||||
{
|
||||
"date" : 1393158814,
|
||||
"name" : "serial.log",
|
||||
"size" : 1798419,
|
||||
"refs": {
|
||||
"resource": "http://example.com/api/logs/serial.log",
|
||||
"download": "http://example.com/downloads/logs/serial.log"
|
||||
}
|
||||
}
|
||||
],
|
||||
"free": 12237201408
|
||||
}
|
||||
|
||||
:statuscode 200: No error
|
||||
:statuscode 403: If the given API token did not have admin rights associated with it
|
||||
|
||||
.. _sec-api-logs-delete:
|
||||
|
||||
Delete a specific logfile
|
||||
=========================
|
||||
|
||||
.. http:delete:: /api/logs/(path:filename)
|
||||
|
||||
Delete the selected log file with name `filename`.
|
||||
|
||||
Returns a :http:statuscode:`204` after successful deletion.
|
||||
|
||||
**Example Request**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/logs/octoprint.log.2014-02-17 HTTP/1.1
|
||||
Host: example.com
|
||||
X-Api-Key: abcdef...
|
||||
|
||||
:param filename: The filename of the log file to delete
|
||||
:statuscode 204: No error
|
||||
:statuscode 403: If the given API token did not have admin rights associated with it
|
||||
:statuscode 404: If the file was not found
|
||||
|
||||
.. _sec-api-logs-datamodel:
|
||||
|
||||
Datamodel
|
||||
=========
|
||||
|
||||
.. _sec-api-logs-datamodel-retrieveresponse:
|
||||
|
||||
Logfile Retrieve Response
|
||||
-------------------------
|
||||
|
||||
.. list-table::
|
||||
:widths: 15 5 10 30
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Multiplicity
|
||||
- Type
|
||||
- Description
|
||||
* - ``files``
|
||||
- 0..*
|
||||
- Array of :ref:`File information items <sec-api-logs-datamodel-fileinfo>`
|
||||
- The list of requested files. Might be an empty list if no files are available
|
||||
* - ``free``
|
||||
- 1
|
||||
- String
|
||||
- The amount of disk space in bytes available in the local disk space (refers to OctoPrint's ``logs`` folder).
|
||||
|
||||
.. _sec-api-logs-datamodel-fileinfo:
|
||||
|
||||
File information
|
||||
----------------
|
||||
|
||||
.. list-table::
|
||||
:widths: 15 5 10 30
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Multiplicity
|
||||
- Type
|
||||
- Description
|
||||
* - ``name``
|
||||
- 1
|
||||
- String
|
||||
- The name of the file
|
||||
* - ``size``
|
||||
- 1
|
||||
- Number
|
||||
- The size of the file in bytes.
|
||||
* - ``date``
|
||||
- 1
|
||||
- Unix timestamp
|
||||
- The timestamp when this file was last modified.
|
||||
* - ``refs``
|
||||
- 1
|
||||
- :ref:`sec-api-logs-datamodel-ref`
|
||||
- References relevant to this file
|
||||
|
||||
.. _sec-api-logs-datamodel-ref:
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
.. list-table::
|
||||
:widths: 15 5 10 30
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Multiplicity
|
||||
- Type
|
||||
- Description
|
||||
* - ``resource``
|
||||
- 1
|
||||
- URL
|
||||
- The resource that represents the file (e.g. for deleting)
|
||||
* - ``download``
|
||||
- 1
|
||||
- URL
|
||||
- The download URL for the file
|
||||
|
|
@ -28,7 +28,7 @@ admin_permission = Permission(RoleNeed("admin"))
|
|||
user_permission = Permission(RoleNeed("user"))
|
||||
|
||||
|
||||
from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection
|
||||
from octoprint.server.util import LargeResponseHandler, ReverseProxied, restricted_access, PrinterStateConnection, admin_validator
|
||||
from octoprint.printer import Printer, getConnectionOptions
|
||||
from octoprint.settings import settings
|
||||
import octoprint.gcodefiles as gcodefiles
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ 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, eventManager, restricted_access, NO_CONTENT
|
||||
from octoprint.server.util import urlForDownload
|
||||
from octoprint.server.api import api
|
||||
|
||||
|
||||
|
|
@ -66,7 +65,7 @@ def _getFileList(origin):
|
|||
file.update({
|
||||
"refs": {
|
||||
"resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=file["name"], _external=True),
|
||||
"download": urlForDownload(FileDestinations.LOCAL, file["name"])
|
||||
"download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + file["name"]
|
||||
}
|
||||
})
|
||||
return files
|
||||
|
|
@ -169,7 +168,7 @@ def uploadGcodeFile(target):
|
|||
"origin": FileDestinations.LOCAL,
|
||||
"refs": {
|
||||
"resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True),
|
||||
"download": urlForDownload(FileDestinations.LOCAL, filename)
|
||||
"download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + filename
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -269,6 +268,5 @@ def deleteGcodeFile(filename, target):
|
|||
else:
|
||||
gcodeManager.removeFile(filename)
|
||||
|
||||
# return an updated list of files
|
||||
return readGcodeFiles()
|
||||
return NO_CONTENT
|
||||
|
||||
|
|
|
|||
|
|
@ -4,35 +4,43 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
|
|||
|
||||
import os
|
||||
|
||||
from flask import request, jsonify, make_response, url_for
|
||||
from flask import request, jsonify, url_for, make_response
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from octoprint.settings import settings
|
||||
|
||||
from octoprint.server import restricted_access
|
||||
from octoprint.server import restricted_access, NO_CONTENT, admin_permission
|
||||
from octoprint.server.util import redirectToTornado
|
||||
from octoprint.server.api import api
|
||||
from octoprint.util import getFreeBytes
|
||||
|
||||
|
||||
@api.route("/logs", methods=["GET"])
|
||||
def getLogData():
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def getLogFiles():
|
||||
files = _getLogFiles()
|
||||
return jsonify(files=files)
|
||||
return jsonify(files=files, free=getFreeBytes(settings().getBaseFolder("logs")))
|
||||
|
||||
|
||||
@api.route("/logs/<filename>", methods=["GET"])
|
||||
@api.route("/logs/<path:filename>", methods=["GET"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def downloadLog(filename):
|
||||
return redirectToTornado(request, url_for("index") + "downloads/logs/" + filename)
|
||||
|
||||
|
||||
@api.route("/logs/<filename>", methods=["DELETE"])
|
||||
@api.route("/logs/<path:filename>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def deleteLog(filename):
|
||||
secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename))
|
||||
if os.path.exists(secure):
|
||||
os.remove(secure)
|
||||
if not os.path.exists(secure):
|
||||
return make_response("File not found: %s" % filename, 404)
|
||||
|
||||
return getLogData()
|
||||
os.remove(secure)
|
||||
|
||||
return NO_CONTENT
|
||||
|
||||
|
||||
def _getLogFiles():
|
||||
|
|
@ -42,10 +50,12 @@ def _getLogFiles():
|
|||
statResult = os.stat(os.path.join(basedir, osFile))
|
||||
files.append({
|
||||
"name": osFile,
|
||||
"size": statResult.st_size,
|
||||
"bytes": statResult.st_size,
|
||||
"date": int(statResult.st_mtime),
|
||||
"url": url_for("index") + "downloads/logs/" + osFile
|
||||
"size": statResult.st_size,
|
||||
"refs": {
|
||||
"resource": url_for(".downloadLog", filename=osFile, _external=True),
|
||||
"download": url_for("index", _external=True) + "downloads/logs/" + osFile
|
||||
}
|
||||
})
|
||||
|
||||
return files
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
# 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.ext.principal import identity_changed, Identity
|
||||
from tornado.web import StaticFileHandler, HTTPError
|
||||
from flask import url_for, make_response, request, current_app
|
||||
from flask.ext.login import login_required, login_user
|
||||
from flask.ext.login import login_required, login_user, current_user
|
||||
from werkzeug.utils import redirect
|
||||
from sockjs.tornado import SockJSConnection
|
||||
|
||||
|
|
@ -21,6 +25,7 @@ import octoprint.server
|
|||
from octoprint.users import ApiUser
|
||||
from octoprint.events import Events
|
||||
|
||||
|
||||
def restricted_access(func, apiEnabled=True):
|
||||
"""
|
||||
If you decorate a view with this, it will ensure that first setup has been
|
||||
|
|
@ -80,11 +85,32 @@ def api_access(func):
|
|||
return decorated_view
|
||||
|
||||
|
||||
def _getUserForApiKey(apikey):
|
||||
if settings().get(["api", "enabled"]) and apikey is not None:
|
||||
if apikey == settings().get(["api", "key"]):
|
||||
# master key was used
|
||||
return ApiUser()
|
||||
else:
|
||||
# user key might have been used
|
||||
return octoprint.server.userManager.findUser(apikey=apikey)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _getApiKey(request):
|
||||
if "apikey" in request.values:
|
||||
# Check Flask GET/POST arguments
|
||||
if hasattr(request, "values") and "apikey" in request.values:
|
||||
return request.values["apikey"]
|
||||
|
||||
# Check Tornado GET/POST arguments
|
||||
if hasattr(request, "arguments") and "apikey" in request.arguments \
|
||||
and len(request.arguments["apikey"]) > 0 and len(request.arguments["apikey"].strip()) > 0:
|
||||
return request.arguments["apikey"]
|
||||
|
||||
# Check Tornado and Flask headers
|
||||
if "X-Api-Key" in request.headers.keys():
|
||||
return request.headers.get("X-Api-Key")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -204,11 +230,15 @@ class LargeResponseHandler(StaticFileHandler):
|
|||
|
||||
CHUNK_SIZE = 16 * 1024
|
||||
|
||||
def initialize(self, path, default_filename=None, as_attachment=False):
|
||||
def initialize(self, path, default_filename=None, as_attachment=False, access_validation=None):
|
||||
StaticFileHandler.initialize(self, path, default_filename)
|
||||
self._as_attachment = as_attachment
|
||||
self._access_validation = access_validation
|
||||
|
||||
def get(self, path, include_body=True):
|
||||
if self._access_validation is not None:
|
||||
self._access_validation(self.request)
|
||||
|
||||
path = self.parse_url_path(path)
|
||||
abspath = os.path.abspath(os.path.join(self.root, path))
|
||||
# os.path.abspath strips a trailing /
|
||||
|
|
@ -273,6 +303,21 @@ class LargeResponseHandler(StaticFileHandler):
|
|||
self.set_header("Content-Disposition", "attachment")
|
||||
|
||||
|
||||
#~~ admin access validator for tornado
|
||||
|
||||
|
||||
# TODO doesnt work for flask right now (no app context of course), try to figure out something
|
||||
def admin_validator(request):
|
||||
apikey = _getApiKey(request)
|
||||
if settings().get(["api", "enabled"]) and apikey is not None:
|
||||
user = _getUserForApiKey(apikey)
|
||||
else:
|
||||
user = current_user
|
||||
|
||||
if user is None or not user.is_authenticated() or not user.is_admin():
|
||||
raise HTTPError(403)
|
||||
|
||||
|
||||
#~~ reverse proxy compatible wsgi middleware
|
||||
|
||||
|
||||
|
|
@ -333,7 +378,3 @@ def redirectToTornado(request, target):
|
|||
redirectUrl += fragment
|
||||
return redirect(redirectUrl)
|
||||
|
||||
|
||||
def urlForDownload(origin, filename):
|
||||
return url_for("index", _external=True) + "downloads/files/" + origin + "/" + filename
|
||||
|
||||
|
|
|
|||
|
|
@ -560,7 +560,7 @@
|
|||
<td class="settings_logs_size" data-bind="text: formatSize(size)"></td>
|
||||
<td class="settings_logs_date" data-bind="text: formatDate(date)"></td>
|
||||
<td class="settings_logs_action">
|
||||
<a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a> | <a href="#" class="icon-download" data-bind="attr: {href: url}"></a>
|
||||
<a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a> | <a href="#" class="icon-download" data-bind="attr: {href: refs.download}"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
|||
Loading…
Reference in a new issue