Compare commits
2 commits
5009cf2826
...
5709cac469
Author | SHA1 | Date | |
---|---|---|---|
5709cac469 | |||
b017108700 |
18 changed files with 294 additions and 92 deletions
|
@ -1,4 +1,5 @@
|
||||||
SECRET_KEY = "SgvzxsO5oPBGInmqsyyGQWAJXkS9"
|
SECRET_KEY = "SgvzxsO5oPBGInmqsyyGQWAJXkS9"
|
||||||
|
UPLOAD_FOLDER = "/home/arrdem/Documents/hobby/programming/source/projects/tentacles/tmp"
|
||||||
|
|
||||||
[db]
|
[db]
|
||||||
uri = "/home/arrdem/Documents/hobby/programming/source/projects/tentacles/tentacles.sqlite3"
|
uri = "/home/arrdem/Documents/hobby/programming/source/projects/tentacles/tentacles.sqlite3"
|
||||||
|
|
|
@ -8,7 +8,7 @@ from flask import Flask, request
|
||||||
import tomllib
|
import tomllib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from tentacles.blueprints import user_ui, printer_ui, api
|
from tentacles.blueprints import user_ui, printer_ui, job_ui, file_ui, api
|
||||||
from tentacles.store import Store
|
from tentacles.store import Store
|
||||||
from tentacles.globals import _ctx, Ctx, ctx
|
from tentacles.globals import _ctx, Ctx, ctx
|
||||||
from tentacles.workers import create_workers
|
from tentacles.workers import create_workers
|
||||||
|
@ -78,6 +78,8 @@ def serve(hostname: str, port: int, config: Path):
|
||||||
# Blueprints
|
# Blueprints
|
||||||
app.register_blueprint(user_ui.BLUEPRINT)
|
app.register_blueprint(user_ui.BLUEPRINT)
|
||||||
app.register_blueprint(printer_ui.BLUEPRINT)
|
app.register_blueprint(printer_ui.BLUEPRINT)
|
||||||
|
app.register_blueprint(job_ui.BLUEPRINT)
|
||||||
|
app.register_blueprint(file_ui.BLUEPRINT)
|
||||||
app.register_blueprint(api.BLUEPRINT)
|
app.register_blueprint(api.BLUEPRINT)
|
||||||
|
|
||||||
# Shove our middleware in there
|
# Shove our middleware in there
|
||||||
|
|
|
@ -2,8 +2,14 @@
|
||||||
|
|
||||||
"""API endpoints supporting the 'ui'."""
|
"""API endpoints supporting the 'ui'."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import Blueprint
|
from tentacles.globals import ctx
|
||||||
|
from tentacles.blueprints.util import requires_admin, requires_auth
|
||||||
|
|
||||||
|
from flask import Blueprint, request, current_app
|
||||||
|
from hashlib import sha3_256
|
||||||
|
|
||||||
BLUEPRINT = Blueprint("api", __name__, url_prefix="/api")
|
BLUEPRINT = Blueprint("api", __name__, url_prefix="/api")
|
||||||
|
|
||||||
|
@ -12,17 +18,25 @@ BLUEPRINT = Blueprint("api", __name__, url_prefix="/api")
|
||||||
# Printers
|
# Printers
|
||||||
#
|
#
|
||||||
# The trick here is handling multipart uploads.
|
# The trick here is handling multipart uploads.
|
||||||
@BLUEPRINT.route("/printer", methods=["POST"])
|
@requires_admin
|
||||||
|
@BLUEPRINT.route("/printers", methods=["POST"])
|
||||||
def create_printer():
|
def create_printer():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/printer", methods=["GET"])
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/printers", methods=["GET"])
|
||||||
|
@BLUEPRINT.route("/printers/", methods=["GET"])
|
||||||
def list_printers():
|
def list_printers():
|
||||||
pass
|
return {
|
||||||
|
"printers": [
|
||||||
|
{"id": p.id, "name": p.name, "url": p.url} for p in ctx.db.list_printers()
|
||||||
|
]
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/printer", methods=["DELETE"])
|
@requires_admin
|
||||||
|
@BLUEPRINT.route("/printers", methods=["DELETE"])
|
||||||
def delete_printer():
|
def delete_printer():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -31,40 +45,100 @@ def delete_printer():
|
||||||
# Files
|
# Files
|
||||||
#
|
#
|
||||||
# The trick here is handling multipart uploads.
|
# The trick here is handling multipart uploads.
|
||||||
@BLUEPRINT.route("/file", methods=["POST"])
|
@requires_auth
|
||||||
def create_file():
|
@BLUEPRINT.route("/files", methods=["POST"])
|
||||||
pass
|
@BLUEPRINT.route("/files/<location>", methods=["POST"])
|
||||||
|
def create_file(location: Optional[str] = None):
|
||||||
|
# This is the important one, because it's the one that PrusaSlicer et all use to upload jobs.
|
||||||
|
|
||||||
|
print(request)
|
||||||
|
print(request.headers)
|
||||||
|
print(request.files)
|
||||||
|
print(request.form)
|
||||||
|
|
||||||
|
if "file" not in request.files:
|
||||||
|
return {"status": "error", "error": "No file to upload"}, 400
|
||||||
|
|
||||||
|
file = request.files["file"]
|
||||||
|
# If the user does not select a file, the browser submits an
|
||||||
|
# empty file without a filename.
|
||||||
|
if not file.filename:
|
||||||
|
return {"status": "error", "error": "No filename provided"}, 400
|
||||||
|
|
||||||
|
elif not file.filename.endswith(".gcode"):
|
||||||
|
return {"status": "error", "error": "Non-gcode file specified"}, 400
|
||||||
|
|
||||||
|
else:
|
||||||
|
digest = sha3_256()
|
||||||
|
digest.update(file.filename.encode())
|
||||||
|
sanitized_filename = digest.hexdigest() + ".gcode"
|
||||||
|
sanitized_path = os.path.join(
|
||||||
|
current_app.config["UPLOAD_FOLDER"], sanitized_filename
|
||||||
|
)
|
||||||
|
file.save(sanitized_path)
|
||||||
|
ctx.db.create_file(ctx.uid, file.filename, sanitized_path)
|
||||||
|
|
||||||
|
return {"status": "ok"}, 202
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/file", methods=["GET"])
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/files", methods=["GET"])
|
||||||
|
@BLUEPRINT.route("/files/", methods=["GET"])
|
||||||
def get_files():
|
def get_files():
|
||||||
pass
|
return {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"id": f.id,
|
||||||
|
"filename": f.filename,
|
||||||
|
"path": f.path,
|
||||||
|
"owner": ctx.uid,
|
||||||
|
"upload_date": f.upload_date,
|
||||||
|
}
|
||||||
|
for f in ctx.db.list_files(ctx.uid)
|
||||||
|
]
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/file", methods=["DELETE"])
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/files", methods=["DELETE"])
|
||||||
def delete_file():
|
def delete_file():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
# Jobs
|
# Jobs
|
||||||
@BLUEPRINT.route("/job", methods=["POST"])
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/jobs", methods=["POST"])
|
||||||
def create_job():
|
def create_job():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/job", methods=["GET"])
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/jobs", methods=["GET"])
|
||||||
def get_jobs():
|
def get_jobs():
|
||||||
pass
|
return {
|
||||||
|
"jobs": [
|
||||||
|
{
|
||||||
|
"id": j.id,
|
||||||
|
"file_id": j.file_id,
|
||||||
|
"started_at": j.started_at,
|
||||||
|
"finished_at": j.finished_at,
|
||||||
|
"printer_id": j.printer_id,
|
||||||
|
}
|
||||||
|
for j in ctx.db.list_jobs()
|
||||||
|
]
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/job", methods=["DELETE"])
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/jobs", methods=["DELETE"])
|
||||||
def delete_job():
|
def delete_job():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
# API tokens
|
# API tokens
|
||||||
@BLUEPRINT.route("/token", methods=["GET"])
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/tokens", methods=["GET"])
|
||||||
def get_tokens():
|
def get_tokens():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
import re
|
||||||
|
|
||||||
|
from tentacles.globals import ctx
|
||||||
|
from .util import requires_auth
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
request,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
flash,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .util import salt, is_logged_in
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
BLUEPRINT = Blueprint("files", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/files", methods=["GET", "POST"])
|
||||||
|
def files():
|
||||||
|
if request.method == "POST":
|
||||||
|
flash("Not supported yet", category="warning")
|
||||||
|
|
||||||
|
return render_template("files.html.j2")
|
32
projects/tentacles/src/python/tentacles/blueprints/job_ui.py
Normal file
32
projects/tentacles/src/python/tentacles/blueprints/job_ui.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
import re
|
||||||
|
|
||||||
|
from tentacles.globals import ctx
|
||||||
|
from .util import requires_auth
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
request,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
flash,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .util import salt, is_logged_in
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
BLUEPRINT = Blueprint("jobs", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@requires_auth
|
||||||
|
@BLUEPRINT.route("/jobs", methods=["GET", "POST"])
|
||||||
|
def jobs():
|
||||||
|
if request.method == "POST":
|
||||||
|
ctx.db.create_job(ctx.uid, int(request.form.get("file_id")))
|
||||||
|
flash("Job created!", category="info")
|
||||||
|
|
||||||
|
return redirect("/")
|
|
@ -1,10 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""Blueprints for HTML serving 'ui'."""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
request,
|
request,
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""Blueprints for HTML serving 'ui'."""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import re
|
import re
|
||||||
|
|
|
@ -31,3 +31,14 @@ def requires_admin(f):
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return _helper
|
return _helper
|
||||||
|
|
||||||
|
|
||||||
|
def requires_auth(f):
|
||||||
|
def _helper(*args, **kwargs):
|
||||||
|
if not ctx.uid:
|
||||||
|
flash("Please log in first", category="error")
|
||||||
|
redirect("/")
|
||||||
|
else:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return _helper
|
||||||
|
|
|
@ -71,6 +71,7 @@ CREATE TABLE IF NOT EXISTS files (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||||
, user_id INTEGER
|
, user_id INTEGER
|
||||||
, filename TEXT
|
, filename TEXT
|
||||||
|
, path TEXT
|
||||||
, upload_date TEXT
|
, upload_date TEXT
|
||||||
, FOREIGN KEY(user_id) REFERENCES user(id)
|
, FOREIGN KEY(user_id) REFERENCES user(id)
|
||||||
);
|
);
|
||||||
|
|
|
@ -274,7 +274,7 @@ class Store(object):
|
||||||
"""
|
"""
|
||||||
SELECT p.id
|
SELECT p.id
|
||||||
FROM printers p
|
FROM printers p
|
||||||
LEFT JOIN jobs j ON p.id = j.printer_id
|
LEFT JOIN (SELECT id, printer_id FROM jobs WHERE finished_at IS NULL) j ON p.id = j.printer_id
|
||||||
WHERE j.id IS NULL
|
WHERE j.id IS NULL
|
||||||
"""
|
"""
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
@ -298,8 +298,8 @@ class Store(object):
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def create_file(self, uid: int, name: str, path: Path) -> int:
|
def create_file(self, uid: int, name: str, path: Path) -> int:
|
||||||
return self._conn.execute(
|
return self._conn.execute(
|
||||||
"INSERT INTO files (user_id, filename, upload_date) VALUES (?, ?, datetime('now')) RETURNING (id)",
|
"INSERT INTO files (user_id, filename, path, upload_date) VALUES (?, ?, ?, datetime('now')) RETURNING (id)",
|
||||||
[uid, name],
|
[uid, name, path],
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
|
@ -341,11 +341,11 @@ class Store(object):
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def list_jobs(self, uid: Optional[int] = None):
|
def list_jobs(self, uid: Optional[int] = None):
|
||||||
"""Enumerate jobs in priority order."""
|
"""Enumerate jobs in priority order."""
|
||||||
cond = f"AND user_id = {uid}" if uid else ""
|
cond = f"user_id = {uid}" if uid else "TRUE"
|
||||||
return self._conn.execute(
|
return self._conn.execute(
|
||||||
f"""
|
f"""
|
||||||
SELECT * FROM jobs
|
SELECT * FROM jobs
|
||||||
WHERE started_at IS NULL AND printer_id IS NULL {cond}
|
WHERE {cond}
|
||||||
ORDER BY priority DESC
|
ORDER BY priority DESC
|
||||||
""",
|
""",
|
||||||
[],
|
[],
|
||||||
|
@ -377,6 +377,7 @@ class Store(object):
|
||||||
[],
|
[],
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
|
@fmap(one)
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def poll_job_queue(self):
|
def poll_job_queue(self):
|
||||||
return self._conn.execute(
|
return self._conn.execute(
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
{% import "macros.html.j2" as macros %}
|
||||||
|
<div class="panel files">
|
||||||
|
<h2>Files</h2>
|
||||||
|
{% with files = ctx.db.list_files(uid=ctx.uid) %}
|
||||||
|
{% if files %}
|
||||||
|
<ul>
|
||||||
|
{% for file in files %}
|
||||||
|
<li class="file">
|
||||||
|
<span class="file-name">{{ file.filename }}</span>
|
||||||
|
<span class="file-controls">
|
||||||
|
{{ macros.start_job(file.id) }}
|
||||||
|
{{ macros.delete_file(file.id) }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
You don't have any files. Upload something!
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel file-upload">
|
||||||
|
<h2>Upload a file</h2>
|
||||||
|
<form method="post" action="/api/files/local" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="select" value="false" />
|
||||||
|
<input type="hidden" name="start" value="false" />
|
||||||
|
<input type="file" name="file" accept=".gcode,text/plain" />
|
||||||
|
<span><input id="submit" type="submit" value="Upload"/></span>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -1,34 +1,8 @@
|
||||||
{% extends "base.html.j2" %}
|
{% extends "base.html.j2" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="panel queue">
|
{% include "jobs_list.html.j2" %}
|
||||||
<h2>Queue</h2>
|
|
||||||
{% with jobs = ctx.db.list_jobs(uid=request.uid) %}
|
|
||||||
{% if jobs %}
|
|
||||||
<ul>
|
|
||||||
{% for job in jobs %}
|
|
||||||
<li></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
No pending tasks. {% if request.uid %}Start something!{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if request.uid %}
|
{% if ctx.uid %}
|
||||||
<div class="panel files">
|
{% include "files_list.html.j2" %}
|
||||||
<h2>Files</h2>
|
|
||||||
{% with files = ctx.db.list_files(uid=request.uid) %}
|
|
||||||
{% if files %}
|
|
||||||
<ul>
|
|
||||||
{% for file in files %}
|
|
||||||
<li></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
You don't have any files. Upload something!
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{% extends "base.html.j2" %}
|
||||||
|
{% block content %}
|
||||||
|
{% include "jobs_list.html.j2" %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<div class="panel queue">
|
||||||
|
<h2>Job queue</h2>
|
||||||
|
{% with jobs = ctx.db.list_jobs(uid=ctx.uid) %}
|
||||||
|
{% if jobs %}
|
||||||
|
<ul>
|
||||||
|
{% for job in jobs %}
|
||||||
|
<li class="job">
|
||||||
|
<span class="job-id">{{job.id}}</span>
|
||||||
|
<span class="job-filename">{{ctx.db.fetch_file(job.file_id).filename}}</span>
|
||||||
|
<span class="job-status">{{ 'pending' if not job.printer_id else 'uploading' if not job.started_at else 'running' if not job.finished_at else job.state }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
No pending tasks. {% if ctx.uid %}Start something!{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% macro start_job(id) %}
|
||||||
|
<form method="post" action="/jobs">
|
||||||
|
<input type="hidden" name="action" value="queue" />
|
||||||
|
<input type="hidden" name="file_id" value="{{ id }}" />
|
||||||
|
<span><input id="submit" type="submit" value="Queue"/></span>
|
||||||
|
</form>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro delete_file(id) %}
|
||||||
|
<form method="post" action="/files">
|
||||||
|
<input type="hidden" name="action" value="delete" />
|
||||||
|
<input type="hidden" name="id" value="{{ id }}" />
|
||||||
|
<span><input id="submit" type="submit" value="Delete"/></span>
|
||||||
|
</form>
|
||||||
|
{% endmacro %}
|
|
@ -1,31 +1,4 @@
|
||||||
{% extends "base.html.j2" %}
|
{% extends "base.html.j2" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="printers">
|
{% include "printers_list.html.j2" %}
|
||||||
<h2>Printers</h2>
|
|
||||||
{% with printers = ctx.db.list_printers() %}
|
|
||||||
{% if printers %}
|
|
||||||
<ul>
|
|
||||||
{% for printer in printers %}
|
|
||||||
{% with id, name, url, _api_key, last_poll, status = printer %}
|
|
||||||
<li class="printer row">
|
|
||||||
<span class="printer-name">{{name}}</span>
|
|
||||||
<span class="printer-url"><code>{{url}}</code></span>
|
|
||||||
<span class="printer-status">{{status}}</span>
|
|
||||||
<span class="printer-date">{{last_poll}}</span>
|
|
||||||
{# FIXME: How should these action buttons work? #}
|
|
||||||
<span class="printer-controls ml-auto">
|
|
||||||
<a class="button" href="/printers/test?id={{id}}">Test</a>
|
|
||||||
<a class="button" href="/printers/edit?id={{id}}">Edit</a>
|
|
||||||
<a class="button" href="/printers/delete?id={{id}}">Remove</a>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% if ctx.is_admin %}<a class="button" href="/printers/add">Add a printer</a>{% endif %}
|
|
||||||
{% else %}
|
|
||||||
No printers available. {% if ctx.is_admin %}<a href="/printers/add">Configure one!</a>{% else %}Ask the admin to configure one!{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<div class="printers">
|
||||||
|
<h2>Printers</h2>
|
||||||
|
{% with printers = ctx.db.list_printers() %}
|
||||||
|
{% if printers %}
|
||||||
|
<ul>
|
||||||
|
{% for printer in printers %}
|
||||||
|
{% with id, name, url, _api_key, last_poll, status = printer %}
|
||||||
|
<li class="printer row">
|
||||||
|
<span class="printer-name">{{name}}</span>
|
||||||
|
<span class="printer-url"><code>{{url}}</code></span>
|
||||||
|
<span class="printer-status">{{status}}</span>
|
||||||
|
<span class="printer-date">{{last_poll}}</span>
|
||||||
|
{# FIXME: How should these action buttons work? #}
|
||||||
|
<span class="printer-controls ml-auto">
|
||||||
|
<a class="button" href="/printers/test?id={{id}}">Test</a>
|
||||||
|
<a class="button" href="/printers/edit?id={{id}}">Edit</a>
|
||||||
|
<a class="button" href="/printers/delete?id={{id}}">Remove</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% if ctx.is_admin %}<a class="button" href="/printers/add">Add a printer</a>{% endif %}
|
||||||
|
{% else %}
|
||||||
|
No printers available. {% if ctx.is_admin %}<a href="/printers/add">Configure one!</a>{% else %}Ask the admin to configure one!{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
|
@ -15,6 +15,7 @@ import logging
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from urllib import parse as urlparse
|
from urllib import parse as urlparse
|
||||||
from tentacles.store import Store
|
from tentacles.store import Store
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from octorest import OctoRest as _OR
|
from octorest import OctoRest as _OR
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
@ -100,12 +101,9 @@ def assign_jobs(db_factory: Callable[[], Store]) -> None:
|
||||||
|
|
||||||
with closing(db_factory()) as db:
|
with closing(db_factory()) as db:
|
||||||
for printer_id in db.list_idle_printers():
|
for printer_id in db.list_idle_printers():
|
||||||
printer = db.fetch_printer(printer_id)
|
if job_id := db.poll_job_queue():
|
||||||
if printer.status != "idle":
|
db.assign_job(job_id, printer_id)
|
||||||
continue
|
print(f"Mapped job {job_id} to printer {printer_id}")
|
||||||
|
|
||||||
if next_job_id := db.poll_job_queue():
|
|
||||||
db.assign_job(next_job_id, printer_id)
|
|
||||||
|
|
||||||
|
|
||||||
@corn_job(timedelta(seconds=5))
|
@corn_job(timedelta(seconds=5))
|
||||||
|
@ -117,9 +115,21 @@ def push_jobs(db_factory: Callable[[], Store]) -> None:
|
||||||
printer = db.fetch_printer(job.printer_id)
|
printer = db.fetch_printer(job.printer_id)
|
||||||
file = db.fetch_file(job.file_id)
|
file = db.fetch_file(job.file_id)
|
||||||
try:
|
try:
|
||||||
client = OctoRest(printer.url, printer.api_key)
|
client = OctoRest(url=printer.url, apikey=printer.api_key)
|
||||||
client.upload(file.filename, select=True, print=True)
|
try:
|
||||||
|
client.upload(file.path)
|
||||||
|
except HTTPError as e:
|
||||||
|
if e.response.status_code == 409:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
client.select(Path(file.path).name)
|
||||||
|
client.start()
|
||||||
db.start_job(job.id)
|
db.start_job(job.id)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Oop")
|
log.exception("Oop")
|
||||||
|
|
||||||
|
@ -132,6 +142,7 @@ def pull_jobs(db_factory: Callable[[], Store]) -> None:
|
||||||
for job in db.list_running_jobs():
|
for job in db.list_running_jobs():
|
||||||
printer = db.fetch_printer(job.printer_id)
|
printer = db.fetch_printer(job.printer_id)
|
||||||
if printer.status != "running":
|
if printer.status != "running":
|
||||||
|
print(f"Job {job.id} finished {printer.status}")
|
||||||
db.finish_job(job.id, printer.status)
|
db.finish_job(job.id, printer.status)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue