From 874adce8f4efdda653c1e60d5b353a3bc816af93 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 27 Mar 2019 18:28:57 +0900 Subject: gerboweb: Initial commit The functionality is there, no design yet --- gerboweb/gerboweb.py | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 gerboweb/gerboweb.py (limited to 'gerboweb/gerboweb.py') diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py new file mode 100644 index 0000000..bf2921a --- /dev/null +++ b/gerboweb/gerboweb.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +# TODO create systemd unit file +# TODO create systemd tmpfiles.d config +# TODO setup ansible deployment +# TODO setup webserver user disk quota + +import tempfile +import uuid +from functools import wraps +from os import path +import os +import sqlite3 + +from flask import Flask, url_for, redirect, session, make_response, render_template, request, send_file, abort +from flask_wtf import FlaskForm +from flask_wtf.file import FileField +from wtforms.fields import RadioField +from wtforms.validators import DataRequired +from werkzeug.utils import secure_filename + +from job_queue import JobQueue + +app = Flask(__name__, static_url_path='/static') +app.config.from_envvar('GERBOWEB_SETTINGS') + +class UploadForm(FlaskForm): + upload_file = FileField(validators=[DataRequired()]) + +class OverlayForm(UploadForm): + upload_file = FileField(validators=[DataRequired()]) + side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')]) + +class ResetForm(FlaskForm): + pass + +job_queue = JobQueue(app.config['JOB_QUEUE_DB']) + +def tempfile_path(namespace): + """ Return a path for a per-session temporary file identified by the given namespace. Create the session tempfile + dir if necessary. The application tempfile dir is controlled via the upload_path config value and not managed by + this function. """ + if not path.isdir(app.config['UPLOAD_PATH']): + os.mkdir(app.config['UPLOAD_PATH']) + sess_tmp = path.join(app.config['UPLOAD_PATH'], session['session_id']) + if not path.isdir(sess_tmp): + os.mkdir(sess_tmp) + + return path.join(sess_tmp, namespace) + +def require_session_id(fun): + @wraps(fun) + def wrapper(*args, **kwargs): + if 'session_id' not in session: + session['session_id'] = str(uuid.uuid4()) + return fun(*args, **kwargs) + return wrapper + +@app.route('/') +@require_session_id +def index(): + forms = { + 'gerber_form': UploadForm(), + 'overlay_form': OverlayForm(), + 'reset_form': ResetForm() } + + for job in ('vector_job', 'render_job'): + if job in session and job_queue[session[job]].finished: + del session[job] + + r = make_response(render_template('index.html', + has_renders = path.isfile(tempfile_path('gerber.zip')), + has_output = path.isfile(tempfile_path('overlay.png')), + **forms)) + if 'vector_job' in session or 'render_job' in session: + r.headers.set('refresh', '10') + return r + +# NOTES about the gerber and overlay file upload routines +# * The maximum upload size is limited by the MAX_CONTENT_LENGTH config setting. +# * The uploaded files are deleted after a while by systemd tmpfiles.d +# TODO: validate this setting applies *after* gzip transport compression + +@app.route('/upload/', methods=['POST']) +@require_session_id +def upload(namespace): + if namespace not in ('gerber', 'overlay'): + return abort(400, 'Invalid upload type') + + upload_form = UploadForm() if namespace == 'gerber' else OverlayForm() + if upload_form.validate_on_submit(): + f = upload_form.upload_file.data + + if namespace == 'gerber': + f.save(tempfile_path('gerber.zip')) + session['filename'] = secure_filename(f.filename) # Cache filename for later download + if 'render_job' in session: + job_queue.drop(session['render_job']) + session['render_job'] = job_queue.enqueue('render', + session_id=session['session_id'], + client=request.remote_addr) + else: # namespace == 'vector' + f.save(tempfile_path('overlay.png')) + + # Re-vectorize if either file has changed + if path.isfile(tempfile_path('gerber.zip')) and path.isfile(tempfile_path('overlay.png')): + if 'vector_job' in session: + job_queue.drop(session['vector_job']) + session['vector_job'] = job_queue.enqueue('vector', + client=request.remote_addr, + session_id=session['session_id'], + side=upload_form.side.data) + + return redirect(url_for('index')) + +@app.route('/render/preview/') +def render_preview(side): + if not side in ('top', 'bottom'): + return abort(400, 'side must be either "top" or "bottom"') + return send_file(tempfile_path(f'render_{side}.small.png')) + +@app.route('/render/download/') +def render_download(side): + if not side in ('top', 'bottom'): + return abort(400, 'side must be either "top" or "bottom"') + return send_file(tempfile_path(f'render_{side}.png'), + mimetype='image/png', + as_attachment=True, + attachment_filename=f'{path.splitext(session["filename"])[0]}_render.png') + +@app.route('/output/download') +def output_download(): + return send_file(tempfile_path('gerber_out.zip'), + mimetype='application/zip', + as_attachment=True, + attachment_filename=f'{path.splitext(session["filename"])[0]}_with_artwork.zip') + +@app.route('/session_reset', methods=['POST']) +@require_session_id +def session_reset(): + if 'render_job' in session: + session['render_job'].abort() + if 'vector_job' in session: + session['vector_job'].abort() + session.clear() + return redirect(url_for('index')) + -- cgit From dabe1d8809348cf82c82b9a871f0509e1403a8cd Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 27 Mar 2019 23:08:20 +0900 Subject: Deployment to digitalocean works --- gerboweb/gerboweb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'gerboweb/gerboweb.py') diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index bf2921a..6b579f0 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -14,7 +14,7 @@ import sqlite3 from flask import Flask, url_for, redirect, session, make_response, render_template, request, send_file, abort from flask_wtf import FlaskForm -from flask_wtf.file import FileField +from flask_wtf.file import FileField, FileRequired from wtforms.fields import RadioField from wtforms.validators import DataRequired from werkzeug.utils import secure_filename @@ -28,7 +28,7 @@ class UploadForm(FlaskForm): upload_file = FileField(validators=[DataRequired()]) class OverlayForm(UploadForm): - upload_file = FileField(validators=[DataRequired()]) + upload_file = FileField(validators=[FileRequired()]) side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')]) class ResetForm(FlaskForm): -- cgit From 84148e368d3804c841d566cd0a8f28263445ef97 Mon Sep 17 00:00:00 2001 From: jaseg Date: Thu, 28 Mar 2019 01:50:07 +0900 Subject: gerboweb: Add HTTPS via letsencrypt --- gerboweb/gerboweb.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'gerboweb/gerboweb.py') diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index 6b579f0..17e03e2 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -1,8 +1,5 @@ #!/usr/bin/env python3 -# TODO create systemd unit file -# TODO create systemd tmpfiles.d config -# TODO setup ansible deployment # TODO setup webserver user disk quota import tempfile -- cgit From 23d392c2f7a744d38a369f8bc91cfd0f215f82b0 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 29 Mar 2019 22:09:16 +0900 Subject: Working on the design --- gerboweb/gerboweb.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'gerboweb/gerboweb.py') diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index 17e03e2..1f8d884 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -9,7 +9,7 @@ from os import path import os import sqlite3 -from flask import Flask, url_for, redirect, session, make_response, render_template, request, send_file, abort +from flask import Flask, url_for, redirect, session, make_response, render_template, request, send_file, abort, flash from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileRequired from wtforms.fields import RadioField @@ -26,7 +26,7 @@ class UploadForm(FlaskForm): class OverlayForm(UploadForm): upload_file = FileField(validators=[FileRequired()]) - side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')]) + side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')], default=lambda: session.get('last_download')) class ResetForm(FlaskForm): pass @@ -56,6 +56,7 @@ def require_session_id(fun): @app.route('/') @require_session_id def index(): + flash(f'Gerber file successfully uploaded.', 'success') forms = { 'gerber_form': UploadForm(), 'overlay_form': OverlayForm(), @@ -108,6 +109,7 @@ def upload(namespace): session_id=session['session_id'], side=upload_form.side.data) + flash(f'{"Gerber" if namespace == "gerber" else "Overlay"} file successfully uploaded.', 'success') return redirect(url_for('index')) @app.route('/render/preview/') @@ -120,10 +122,12 @@ def render_preview(side): def render_download(side): if not side in ('top', 'bottom'): return abort(400, 'side must be either "top" or "bottom"') + + session['last_download'] = side return send_file(tempfile_path(f'render_{side}.png'), mimetype='image/png', as_attachment=True, - attachment_filename=f'{path.splitext(session["filename"])[0]}_render.png') + attachment_filename=f'{path.splitext(session["filename"])[0]}_render_{side}.png') @app.route('/output/download') def output_download(): -- cgit From 6b4eac36d6ff46881b26a36556a07280ccd69783 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 30 Mar 2019 04:01:03 +0900 Subject: gerboweb: Initial design revision --- gerboweb/gerboweb.py | 69 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 28 deletions(-) (limited to 'gerboweb/gerboweb.py') diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index 1f8d884..a276d74 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -26,7 +26,8 @@ class UploadForm(FlaskForm): class OverlayForm(UploadForm): upload_file = FileField(validators=[FileRequired()]) - side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')], default=lambda: session.get('last_download')) + side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')], + default=lambda: session.get('side_selected', session.get('last_download'))) class ResetForm(FlaskForm): pass @@ -56,7 +57,6 @@ def require_session_id(fun): @app.route('/') @require_session_id def index(): - flash(f'Gerber file successfully uploaded.', 'success') forms = { 'gerber_form': UploadForm(), 'overlay_form': OverlayForm(), @@ -79,37 +79,49 @@ def index(): # * The uploaded files are deleted after a while by systemd tmpfiles.d # TODO: validate this setting applies *after* gzip transport compression -@app.route('/upload/', methods=['POST']) +def vectorize(): + if 'vector_job' in session: + job_queue.drop(session['vector_job']) + session['vector_job'] = job_queue.enqueue('vector', + client=request.remote_addr, + session_id=session['session_id'], + side=session['side_selected']) + +def render(): + if 'render_job' in session: + job_queue.drop(session['render_job']) + session['render_job'] = job_queue.enqueue('render', + session_id=session['session_id'], + client=request.remote_addr) + +@app.route('/upload/gerber', methods=['POST']) @require_session_id -def upload(namespace): - if namespace not in ('gerber', 'overlay'): - return abort(400, 'Invalid upload type') +def upload_gerber(): + upload_form = UploadForm() + if upload_form.validate_on_submit(): + f = upload_form.upload_file.data + f.save(tempfile_path('gerber.zip')) + session['filename'] = secure_filename(f.filename) # Cache filename for later download - upload_form = UploadForm() if namespace == 'gerber' else OverlayForm() + render() + if path.isfile(tempfile_path('overlay.png')): # Re-vectorize when gerbers change + vectorize() + + flash(f'Gerber file successfully uploaded.', 'success') + return redirect(url_for('index')) + +@app.route('/upload/overlay', methods=['POST']) +@require_session_id +def upload_overlay(): + upload_form = OverlayForm() if upload_form.validate_on_submit(): f = upload_form.upload_file.data + f.save(tempfile_path('overlay.png')) + session['side_selected'] = upload_form.side.data + + vectorize() - if namespace == 'gerber': - f.save(tempfile_path('gerber.zip')) - session['filename'] = secure_filename(f.filename) # Cache filename for later download - if 'render_job' in session: - job_queue.drop(session['render_job']) - session['render_job'] = job_queue.enqueue('render', - session_id=session['session_id'], - client=request.remote_addr) - else: # namespace == 'vector' - f.save(tempfile_path('overlay.png')) - - # Re-vectorize if either file has changed - if path.isfile(tempfile_path('gerber.zip')) and path.isfile(tempfile_path('overlay.png')): - if 'vector_job' in session: - job_queue.drop(session['vector_job']) - session['vector_job'] = job_queue.enqueue('vector', - client=request.remote_addr, - session_id=session['session_id'], - side=upload_form.side.data) - - flash(f'{"Gerber" if namespace == "gerber" else "Overlay"} file successfully uploaded.', 'success') + flash(f'Overlay file successfully uploaded.', 'success') return redirect(url_for('index')) @app.route('/render/preview/') @@ -144,5 +156,6 @@ def session_reset(): if 'vector_job' in session: session['vector_job'].abort() session.clear() + flash('Session reset', 'success'); return redirect(url_for('index')) -- cgit From a846d39bc88e0c03402b20790b04762666bc055f Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 2 Apr 2019 04:34:57 +0900 Subject: gerboweb: Fix job queue handling --- gerboweb/gerboweb.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) (limited to 'gerboweb/gerboweb.py') diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index a276d74..7e952a4 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -63,8 +63,12 @@ def index(): 'reset_form': ResetForm() } for job in ('vector_job', 'render_job'): - if job in session and job_queue[session[job]].finished: - del session[job] + if job in session: + job = job_queue[session[job]] + if job.finished: + if job.result != 0: + flash(f'Error processing gerber files', 'success') # FIXME make this an error, add CSS + del session[job] r = make_response(render_template('index.html', has_renders = path.isfile(tempfile_path('gerber.zip')), @@ -81,7 +85,7 @@ def index(): def vectorize(): if 'vector_job' in session: - job_queue.drop(session['vector_job']) + job_queue[session['vector_job']].abort() session['vector_job'] = job_queue.enqueue('vector', client=request.remote_addr, session_id=session['session_id'], @@ -89,7 +93,7 @@ def vectorize(): def render(): if 'render_job' in session: - job_queue.drop(session['render_job']) + job_queue[session['render_job']].abort() session['render_job'] = job_queue.enqueue('render', session_id=session['session_id'], client=request.remote_addr) @@ -115,6 +119,7 @@ def upload_gerber(): def upload_overlay(): upload_form = OverlayForm() if upload_form.validate_on_submit(): + # FIXME raise error when no side selected f = upload_form.upload_file.data f.save(tempfile_path('overlay.png')) session['side_selected'] = upload_form.side.data @@ -152,9 +157,9 @@ def output_download(): @require_session_id def session_reset(): if 'render_job' in session: - session['render_job'].abort() + job_queue[session['render_job']].abort() if 'vector_job' in session: - session['vector_job'].abort() + job_queue[session['vector_job']].abort() session.clear() flash('Session reset', 'success'); return redirect(url_for('index')) -- cgit From a54abadf01e9e543b416ed09e8500ed71170342c Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 3 Apr 2019 23:53:04 +0900 Subject: gerbolyze fixes, clippy experiments --- gerboweb/gerboweb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'gerboweb/gerboweb.py') diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index 7e952a4..2633e7b 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -62,13 +62,13 @@ def index(): 'overlay_form': OverlayForm(), 'reset_form': ResetForm() } - for job in ('vector_job', 'render_job'): - if job in session: - job = job_queue[session[job]] + for job_type in ('vector_job', 'render_job'): + if job_type in session: + job = job_queue[session[job_type]] if job.finished: if job.result != 0: flash(f'Error processing gerber files', 'success') # FIXME make this an error, add CSS - del session[job] + del session[job_type] r = make_response(render_template('index.html', has_renders = path.isfile(tempfile_path('gerber.zip')), -- cgit