aboutsummaryrefslogtreecommitdiff
path: root/gerboweb/gerboweb.py
blob: 7e952a48d578c2701df3283fdb652c98a5ac869c (plain)
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 in ('vector_job', 'render_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')),
            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'))