Finalize server implementation, improve web frontend

This commit is contained in:
Kumi 2018-10-18 19:04:05 +02:00
parent 6b69da393c
commit ca05257c79
9 changed files with 6910 additions and 0 deletions

2
config.cfg Normal file
View file

@ -0,0 +1,2 @@
[Server]
workdir = /tmp/panosteal

40
connections.py Normal file
View file

@ -0,0 +1,40 @@
import cgi
class IllegalMethodException(BaseException):
pass
class InvalidArgumentException(BaseException):
pass
class Request:
def __init__(self, env = None):
if env:
self.fromEnv(env)
def fromEnv(self, env):
if env["REQUEST_METHOD"] == "POST":
self.args = cgi.parse_qs(env['wsgi.input'].readline().decode(), True)
elif env["REQUEST_METHOD"] == "GET":
self.args = cgi.parse_qs(env['QUERY_STRING'], True)
else:
raise IllegalMethodException()
self.path = env["PATH_INFO"].split("/")
while "" in self.path:
self.path.remove("")
try:
self.endpoint = self.path[0]
except:
self.endpoint = "index.html"
class Response:
def __init__(self, status, ctype, content):
self.status = status
self.headers = [("Content-Type", ctype)]
self.content = content
def addHeader(self, name, value):
self.headers.append((name, value))

130
daemon.py Normal file
View file

@ -0,0 +1,130 @@
import cgi
from connections import Request, Response
import mimetypes
import handler
import uuid
import configparser
import os
import glob
HTTP200 = "200 OK"
HTTP202 = "202 Accepted"
HTTP400 = "400 Bad Request"
HTTP404 = "404 File Not Found"
HTTP405 = "405 Method Not Allowed"
HTTP500 = "500 Internal Server Error"
HTML = "text/html"
JSON = "application/json"
XML = "text/xml"
TEXT = "text/plain"
def static(req):
try:
content = open("server/static/" + req.endpoint, "rb").read()
code = HTTP200
ctype = mimetypes.guess_type(req.endpoint)[0]
except:
code = HTTP404
content = "<h1>404 File Not Found</h1>"
content += """The file you requested was not found on the server.
Check the URL maybe?"""
ctype = HTML
content = content.encode()
return Response(code, ctype, content)
def addjob(req):
jobid = str(uuid.uuid4())
config = configparser.ConfigParser()
try:
title = req.args["title"][0].replace(" ", "_") or "output"
except:
title = "output"
try:
rx = req.args["rx"][0]
except:
rx = 0
try:
ry = req.args["ry"][0]
except:
ry = 0
try:
rz = req.args["rz"][0]
except:
rz = 0
try:
width = req.args["width"][0]
except:
width = 3840
try:
height = req.args["height"][0]
except:
height = 1920
config["Job"] = {
"url": req.args["url"][0],
"title": title,
"rx": rx,
"ry": ry,
"rz": rz,
"width": width,
"height": height
}
with open("/tmp/panosteal/" + jobid, "w") as outfile:
config.write(outfile)
content = jobid.encode()
ctype = TEXT
status = HTTP200
return Response(status, ctype, content)
def getjob(req):
jobid = req.path[-1]
content_disposition = None
if glob.glob("/tmp/panosteal/%s---*" % jobid):
content = open(glob.glob("/tmp/panosteal/%s---*" % jobid)[0], "rb").read()
code = HTTP200
ctype = mimetypes.guess_type("any.png")[0]
content_disposition = glob.glob("/tmp/panosteal/%s---*" % jobid)[0].split("---")[-1]
elif os.path.isfile("/tmp/panosteal/%s.err" % jobid):
content = "<h1>500 Internal Server Error</h1>".encode()
code = HTTP500
ctype = HTML
elif not os.path.isfile("/tmp/panosteal/%s" % jobid):
content = "<h1>404 File Not Found</h1>".encode()
code = HTTP404
ctype = HTML
else:
content = "".encode()
code = HTTP202
ctype = HTML
res = Response(code, ctype, content)
if content_disposition:
res.addHeader("Content-Disposition", 'attachment; filename="%s"' % content_disposition)
return res
def application(env, re):
req = Request(env)
if req.endpoint.lower() == "addjob":
handler = addjob
elif req.endpoint.lower() == "getjob":
handler = getjob
else:
handler = static
res = handler(req)
re(res.status, res.headers)
yield res.content

6
gunicorn.cfg Normal file
View file

@ -0,0 +1,6 @@
import multiprocessing
name = "PANOSTEAL"
bind = "0.0.0.0:3360"
workers = multiprocessing.cpu_count() * 4

4
run.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
gunicorn -c gunicorn.cfg daemon

61
server/hallmonitor.py Normal file
View file

@ -0,0 +1,61 @@
#!/usr/bin/env python3
import pyinotify
import os
import subprocess
import configparser
import handler
import time
import glob
processes = {}
def handleIncoming(filename):
time.sleep(1)
config = configparser.ConfigParser()
config.read(filename)
p = subprocess.Popen(["./handler.py", config["Job"]["url"], "--title",
filename.split("/")[-1] + "---" + config["Job"]["title"],
"--rotation", config["Job"]["rx"], config["Job"]["ry"],
config["Job"]["rz"], "--resolution", config["Job"]["width"],
config["Job"]["height"], "--output", "/tmp/panosteal"])
processes[filename] = [p, time.time()]
class createHandler(pyinotify.ProcessEvent):
def process_IN_CREATE(self, event):
if "." not in event.pathname:
try:
handleIncoming(event.pathname)
except Exception as e:
raise
with open(event.pathname + ".err", "w") as errorfile:
errorfile.write("")
print(e)
def run():
global processes
watch = pyinotify.WatchManager()
worker = pyinotify.ThreadedNotifier(watch, createHandler())
worker.start()
path = watch.add_watch("/tmp/panosteal", pyinotify.IN_CREATE)
while True:
try:
for process, details in processes.items():
if glob.glob("/tmp/panosteal/%s---*" % process):
del processes[process]
continue
if time.time() - details[1] > 1200:
details[0].kill()
if details[0].returncode is not None:
del processes[process]
with open("/tmp/panosteal/%s.err" % process, "w") as errorfile:
errorfile.write("")
except Exception as e:
raise
finally:
time.sleep(30)
if __name__ == "__main__":
run()

94
server/static/index.html Normal file
View file

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" type="text/css">
<link rel="stylesheet" href="theme.css" type="text/css">
</head>
<body>
<div class="py-5">
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="">Panorama Image Export</h1>
</div>
</div>
</div>
</div>
<div class="py-5">
<div class="container">
<div class="row">
<div class="col-md-12">
<form class="" id="theform">
<div class="form-group"> <label>URL</label> <input type="text" class="form-control" placeholder="https://example.com/0/0/0_0.jpg" name="url" required="required"> <small class="form-text text-muted">URL of an image contained in a krpano panorama or of a website containing a krpano panorama</small> </div>
<div class="form-group"> <label>Title</label> <input type="" class="form-control" placeholder="1234 - Shiny Place" name="title"> </div>
<div class="form-group"> <label>Transposition<br></label><select class="custom-control custom-select" name="transpose">
<option value="1" selected="True">Default: Flip left-right (mirror)</option>
<option value="0">No transposition</option>
</select> </div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<script>
function lockform() {
$("#theform :input").prop("disabled", true);
}
function unlockform() {
$("#theform :input").prop("disabled", false);
}
$('#theform').submit(function(event){
event.preventDefault();
if(this.checkValidity()) {
$.ajax({
type: "POST",
url: "/addjob",
data: $('#theform').serialize(),
success: function(msg){
lockform();
interval = setInterval(checkServerForFile,1000,msg);
function checkServerForFile(jobid) {
$.ajax({
type: "GET",
cache: false,
url: "/getjob/" + jobid,
statusCode: {
404: function() {
clearInterval(interval);
unlockform();
},
200: function() {
clearInterval(interval);
unlockform();
window.location.href = "/getjob/" + jobid
},
500: function() {
clearInterval(interval);
unlockform();
}
}
});
}
}
});}
});
</script>
</body>
</html>

6433
server/static/theme.css Normal file

File diff suppressed because it is too large Load diff

140
server/static/theme.scss Normal file
View file

@ -0,0 +1,140 @@
/*!
* Based on Bootstrap (https://getbootstrap.com)
*/
// Options
//
// Quickly modify global styling by enabling or disabling optional features.
$enable-rounded: true !default;
$enable-shadows: true;
$enable-transitions: true;
$enable-hover-media-query: false;
$enable-grid-classes: true;
$enable-print-styles: true;
// Variables
//
// Colors
$theme-colors: (
primary: #12bbad,
secondary: #4f70ce,
light: #f3f3f3,
dark: #151515,
info: #ccc,
success: #28a745,
warning: #ffc107,
danger: #dc3545
);
$body-bg: white;
$body-color: #333;
$body-color-inverse: invert($body-color) !default;
$link-color: #12bbad;
// Fonts
$font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$headings-font-family: $font-family-base;
$display-font-family: $font-family-base;
$font-weight-normal: 200;
$headings-font-weight: 200;
$lead-font-size: 1.30rem;
$spacer: 1.5rem;
@import 'bootstrap-4.1.3';
html,body {
height:100%;
}
.cover {
min-height:100%;
display:flex;
align-items:center
}
.bg-gradient {
overflow: hidden;
color: color-yiq(map-get($theme-colors, 'primary'));
background: linear-gradient(-30deg, theme-color("secondary") 0%, theme-color("primary") 50%, theme-color("primary") 100%);
}
.filter-dark {
overflow: hidden;
position: relative;
color: color-yiq(map-get($theme-colors, 'dark'));
&:before {
position: absolute;
top:0px;
left:0px;
width:100%;
height: 100%;
content: ' ';
background: rgba(map-get($theme-colors, 'dark'), 0.75);
}
}
.filter-light {
overflow: hidden;
position: relative;
color: color-yiq(map-get($theme-colors, 'light'));
&:before {
position: absolute;
top:0px;
left:0px;
width:100%;
height: 100%;
content: ' ';
background: rgba(map-get($theme-colors, 'light'),0.75);
}
}
.filter-color {
overflow: hidden;
position: relative;
color: color-yiq(map-get($theme-colors, 'primary'));
&:before {
position: absolute;
top:0px;
left:0px;
width:100%;
height: 100%;
content: ' ';
background: rgba(map-get($theme-colors, 'primary'), 0.75);
}
}
.filter-gradient {
overflow: hidden;
position: relative;
color: color-yiq(map-get($theme-colors, 'primary'));
&:before {
position: absolute;
top:0px;
left:0px;
width:100%;
height: 100%;
content: ' ';
background: linear-gradient(-30deg, transparentize(theme-color("secondary"), 0.1) 0%, transparentize(theme-color("primary"), 0.1) 50%, transparentize(theme-color("primary"), 0.05) 100%);
}
}
.filter-fade-in {
overflow: hidden;
position: relative;
&:before {
position: absolute;
top:0px;
left:0px;
width:100%;
height: 100%;
content: ' ';
background: linear-gradient($body-bg, transparentize($body-bg, 0.2),transparentize($body-bg, 0.9),transparentize($body-bg, 1));
}
}