zpaste/wastebin/daemon.py

196 lines
5.8 KiB
Python
Raw Normal View History

2019-01-28 22:23:35 -08:00
import os
import cherrypy
import logging
import re
2019-06-06 08:51:07 -07:00
from urllib.parse import urlparse
import ZODB
from relstorage.storage import RelStorage
from relstorage.options import Options
from relstorage.adapters.mysql import MySQLAdapter
import persistent
import persistent.list
import ZODB.FileStorage
import persistent.mapping
import BTrees.OOBTree
def pmap():
return persistent.mapping.PersistentMapping()
class Database(object):
def __init__(self, storage):
self.db = ZODB.DB(storage)
self.init_db()
@staticmethod
def from_uri(uri):
"""
Return a database backed by the storage specified by the passed uri. URIs containing a scheme (scheme://) will
be checked against installed adapters. Schemeless URIs are assumed to be a file path for flat file storage.
"""
parsed = urlparse(uri)
storage = None
if parsed.scheme:
mysql = MySQLAdapter(host=parsed.hostname, port=parsed.port,
user=parsed.username, passwd=parsed.password,
db=parsed.path[1:], options=Options(keep_history=False))
storage = RelStorage(adapter=mysql)
else:
storage = ZODB.FileStorage.FileStorage(uri)
if storage is None:
raise Exception(f"Unsupported uri {uri}")
return Database(storage)
def init_db(self):
with self.db.transaction() as c:
if "pastes" not in c.root():
c.root.pastes = BTrees.OOBTree.BTree()
def loadpaste(self, name):
with self.db.transaction() as c:
return c.root.pastes[name].value
def writepaste(self, name, contents):
with self.db.transaction() as c:
try:
paste = c.root.pastes[name]
paste.value = contents
except KeyError:
paste = Paste(contents)
c.root.pastes[name] = paste
def delpaste(self, name):
with self.db.transaction() as c:
del c.root.pastes[name]
def iterpastes(self, prefix=None):
with self.db.transaction() as c:
for name, value in c.root.pastes.items():
if prefix and not name.startswith(prefix):
continue
yield (name, value, )
class Paste(persistent.Persistent):
def __init__(self, value):
self.value = value
2019-01-28 22:23:35 -08:00
PAGE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wastebin</title>
</head>
<body>
<form action="/make" method="post">
<textarea name="contents" rows="30" cols="120">{data}</textarea><br />
<input type="text" name="name" placeholder="url name" value="{load}" /><br />
<input type="submit" value="Go">
</form>
</body>
</html>
"""
2019-06-06 08:51:07 -07:00
RE_NAME_RAW = r'^[a-z0-9_\-/]+$'
RE_NAME = re.compile(RE_NAME_RAW)
2019-01-28 22:23:35 -08:00
class WasteWeb(object):
2019-06-06 08:51:07 -07:00
def __init__(self, db):
self.db = db
2019-01-28 22:23:35 -08:00
@cherrypy.expose
def index(self, load=None):
data = ""
if load:
2019-06-06 08:51:07 -07:00
try:
data = self.db.loadpaste(load)
except KeyError:
raise cherrypy.HTTPError(404)
2019-01-28 22:23:35 -08:00
yield PAGE.format(data=data.replace("<", "&lt;"), load=load or "")
@cherrypy.expose
def make(self, name, contents):
2019-06-06 08:51:07 -07:00
if not RE_NAME.match(name):
raise cherrypy.HTTPError(400, f"paste name must match {RE_NAME_RAW}")
2022-02-01 23:13:47 -08:00
self.db.writepaste(name, contents.replace("\r", ""))
2019-06-06 08:51:07 -07:00
raise cherrypy.HTTPRedirect("/" + name)
2019-01-28 22:23:35 -08:00
@cherrypy.expose
def default(self, *args):
2019-06-06 08:51:07 -07:00
try:
if cherrypy.request.method == "DELETE":
self.db.delpaste(args[0])
return "OK"
else:
cherrypy.response.headers['Content-Type'] = 'text/plain'
return self.db.loadpaste(args[0]).encode("utf-8")
except KeyError:
raise cherrypy.HTTPError(404)
2019-01-29 16:07:14 -08:00
@cherrypy.expose
2019-06-06 08:51:07 -07:00
def search(self, prefix=""):
cherrypy.response.headers['Content-Type'] = 'text/plain'
2019-01-28 22:23:35 -08:00
2019-06-06 08:51:07 -07:00
def _work():
for name, _ in self.db.iterpastes(prefix):
yield name + "\n"
return _work()
2019-01-28 22:23:35 -08:00
def main():
import argparse
import signal
2019-06-06 08:51:07 -07:00
parser = argparse.ArgumentParser(description="basic pastebin",
epilog="supprted databases are file paths and mysql://")
2019-01-28 22:23:35 -08:00
2019-06-06 08:51:07 -07:00
parser.add_argument('-p', '--port', default=int(os.environ.get("PASTE_PORT", 8080)), type=int, help="http port")
parser.add_argument('-d', '--database', default=os.environ.get("PASTE_DB", None), help="database uri")
2019-01-28 22:23:35 -08:00
parser.add_argument('--debug', action="store_true", help="enable development options")
args = parser.parse_args()
2019-06-06 08:51:07 -07:00
if not args.database:
parser.error("the following arguments are required: -d/--database")
2019-01-28 22:23:35 -08:00
logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING,
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
2019-06-06 08:51:07 -07:00
web = WasteWeb(Database.from_uri(args.database))
2019-01-28 22:23:35 -08:00
cherrypy.tree.mount(web, '/', {'/': {'tools.trailing_slash.on': False}})
cherrypy.config.update({
2019-06-06 08:51:07 -07:00
"tools.sessions.on": False,
"server.socket_host": "0.0.0.0",
"server.socket_port": args.port,
"server.thread_pool": 5,
"engine.autoreload.on": args.debug,
"log.screen": True
2019-01-28 22:23:35 -08:00
})
def signal_handler(signum, stack):
logging.critical('Got sig {}, exiting...'.format(signum))
cherrypy.engine.exit()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
cherrypy.engine.start()
cherrypy.engine.block()
finally:
logging.info("API has shut down")
cherrypy.engine.exit()
if __name__ == '__main__':
main()