155 lines
4.5 KiB
Python
155 lines
4.5 KiB
Python
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, logo: str = None):
|
|
self._name = name
|
|
self._tempdir = Path(tempdir)
|
|
|
|
# sanitize input
|
|
if "/" in logo:
|
|
raise ValueError("invalid logo name")
|
|
self._logo = Path(logo).name
|
|
|
|
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/{self._logo}.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", methods=["POST"])
|
|
async def generate():
|
|
try:
|
|
# support both modern JSON body requests as well as classic forms
|
|
form_data = await request.form
|
|
json_data = await request.json
|
|
|
|
if form_data:
|
|
data = form_data
|
|
|
|
elif json_data:
|
|
data = json_data
|
|
|
|
else:
|
|
raise ValueError
|
|
|
|
name = data["name"]
|
|
logo = data.get("logo", None)
|
|
|
|
except (TypeError, KeyError, ValueError):
|
|
abort(400)
|
|
return
|
|
|
|
async with semaphore:
|
|
with tempfile.TemporaryDirectory(prefix="fablab-bottle-clip-generator-") as tempdir:
|
|
generator = Generator(name, tempdir, logo)
|
|
|
|
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)
|
|
|
|
out_filename = secure_filename(name)
|
|
|
|
# a tester found that, if only special characters (like _) are sent to this method, it will return an
|
|
# empty string
|
|
# to improve UX, we replace empty strings with a _
|
|
if not out_filename:
|
|
out_filename = "_"
|
|
|
|
# to further improve the UX, let's add some prefix and the .stl suffix
|
|
out_filename = f"bottle-clip-{out_filename}.stl"
|
|
|
|
# using secure_filename allows us to send the file to the user with some safe yet reasonably
|
|
# identifiable filename
|
|
response = await send_file(
|
|
bytes_io,
|
|
mimetype="model/stl",
|
|
as_attachment=True,
|
|
attachment_filename=out_filename,
|
|
)
|
|
|
|
# avoid the need for a content-disposition parser in the client code
|
|
response.headers["download-filename"] = out_filename
|
|
|
|
return response
|
|
|
|
|
|
@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())
|