aboutsummaryrefslogtreecommitdiff
path: root/gerboweb/gerboweb.py
blob: 17e03e28ea674d3cbad9a36b98a670fd5d29dfa9 (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
#!/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
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')])

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/<namespace>', 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/<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"')
    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'))