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