Finalize server implementation, improve web frontend
This commit is contained in:
parent
6b69da393c
commit
ca05257c79
9 changed files with 6910 additions and 0 deletions
2
config.cfg
Normal file
2
config.cfg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[Server]
|
||||||
|
workdir = /tmp/panosteal
|
40
connections.py
Normal file
40
connections.py
Normal 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
130
daemon.py
Normal 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
6
gunicorn.cfg
Normal 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
4
run.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
gunicorn -c gunicorn.cfg daemon
|
||||||
|
|
61
server/hallmonitor.py
Normal file
61
server/hallmonitor.py
Normal 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
94
server/static/index.html
Normal 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
6433
server/static/theme.css
Normal file
File diff suppressed because it is too large
Load diff
140
server/static/theme.scss
Normal file
140
server/static/theme.scss
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue