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 $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/") 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())