Initial commit
This commit is contained in:
124
app/app.py
Normal file
124
app/app.py
Normal file
@ -0,0 +1,124 @@
|
||||
import asyncio
|
||||
import io
|
||||
import shutil
|
||||
import tempfile
|
||||
from distutils.dir_util import copy_tree
|
||||
from pathlib import Path
|
||||
|
||||
from quart import Quart, abort, send_file, render_template, request
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
app = Quart(__name__, static_folder="static", template_folder="templates")
|
||||
|
||||
|
||||
# allow at most these many scad processes in parallel
|
||||
semaphore = asyncio.Semaphore(2)
|
||||
|
||||
|
||||
def package_path() -> Path:
|
||||
return Path(__file__).parent
|
||||
|
||||
|
||||
class Generator:
|
||||
GENERATOR_SCAD_FILE_NAME = "generator.scad"
|
||||
GENERATED_STL_FILE_NAME = "generated.stl"
|
||||
|
||||
def __init__(self, name: str, tempdir: Path | str):
|
||||
self._name = name
|
||||
self._tempdir = Path(tempdir)
|
||||
|
||||
def _generate_scad_template(self) -> str:
|
||||
return f"""
|
||||
use <bottle-clip.scad>
|
||||
$fn=180;
|
||||
// one name tag for 0.5l Club Mate and similar bottles
|
||||
bottle_clip(name="{self._name}", logo="thing-logos/fablab-cube2.dxf");
|
||||
"""
|
||||
|
||||
def _generate_files_in_temp_dir(self):
|
||||
copy_tree(str(package_path() / "openscad"), str(self._tempdir))
|
||||
|
||||
with open(self._tempdir / self.GENERATOR_SCAD_FILE_NAME, "w") as f:
|
||||
f.write(self._generate_scad_template())
|
||||
|
||||
async def generate_stl(self) -> str:
|
||||
self._generate_files_in_temp_dir()
|
||||
|
||||
openscad_path = shutil.which("openscad")
|
||||
|
||||
if not openscad_path:
|
||||
abort(500)
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
openscad_path,
|
||||
self.GENERATOR_SCAD_FILE_NAME,
|
||||
"-o",
|
||||
self.GENERATED_STL_FILE_NAME,
|
||||
# "--hardwarnings",
|
||||
cwd=self._tempdir,
|
||||
)
|
||||
|
||||
await proc.wait()
|
||||
|
||||
if proc.returncode != 0:
|
||||
abort(500)
|
||||
|
||||
return self.GENERATED_STL_FILE_NAME
|
||||
|
||||
|
||||
@app.route("/generate/<name>")
|
||||
async def generate_rest(name: str):
|
||||
async with semaphore:
|
||||
with tempfile.TemporaryDirectory(prefix="fablab-bottle-clip-generator-") as tempdir:
|
||||
generator = Generator(name, tempdir)
|
||||
|
||||
generated_stl_file_name = await generator.generate_stl()
|
||||
|
||||
# to be able to use send_file with a temporary directory, we need buffer the entire file in memory
|
||||
# before the context manager gets to delete the dir
|
||||
bytes_io = io.BytesIO()
|
||||
|
||||
with open(Path(tempdir) / generated_stl_file_name, "rb") as f:
|
||||
while True:
|
||||
data = f.read(4096)
|
||||
if not data:
|
||||
break
|
||||
bytes_io.write(data)
|
||||
|
||||
# using secure_filename allows us to send the file to the user with some safe yet reasonably
|
||||
# identifiable filename
|
||||
return await send_file(
|
||||
bytes_io,
|
||||
mimetype="model/stl",
|
||||
as_attachment=True,
|
||||
attachment_filename=secure_filename(f"{name}.stl")
|
||||
)
|
||||
|
||||
|
||||
# aside from the RESTful API above, we need a "traditional" HTML forms compatible end point
|
||||
@app.route("/generate")
|
||||
async def generate_form():
|
||||
try:
|
||||
name = request.args["name"]
|
||||
except KeyError:
|
||||
abort(400)
|
||||
return
|
||||
|
||||
return await generate_rest(name)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def index():
|
||||
return await render_template("index.html")
|
||||
|
||||
|
||||
async def test():
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
generator = Generator("testabc", td)
|
||||
generated_stl_file_name = await generator.generate_stl()
|
||||
shutil.copy(Path(td) / generated_stl_file_name, ".")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(test())
|
Reference in New Issue
Block a user