1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
#!/usr/bin/env python3
# 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, flash
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
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=[FileRequired()])
side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')],
default=lambda: session.get('side_selected', session.get('last_download')))
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_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_type]
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
def vectorize():
if 'vector_job' in session:
job_queue[session['vector_job']].abort()
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[session['render_job']].abort()
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_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
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():
# 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
vectorize()
flash(f'Overlay file successfully uploaded.', 'success')
return redirect(url_for('index'))
@app.route('/render/preview/<side>')
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/<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_{side}.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:
job_queue[session['render_job']].abort()
if 'vector_job' in session:
job_queue[session['vector_job']].abort()
session.clear()
flash('Session reset', 'success');
return redirect(url_for('index'))
|