#!/usr/bin/env python # index.py for a rudimentary key/value server by David Blume import os import sys import time import datetime import cgi, cgitb import filelock import tempfile import yaml # You may need to "pip install pyyaml" import texttime cgitb.enable(display=0, logdir="/tmp") store_name = 'store.txt' def read_file(full_pathname): """ The file must be a YAML file containing a dict. """ store = dict() if os.path.isfile(full_pathname): try: with filelock.FileLock(full_pathname): with open(full_pathname, 'r') as f: store = yaml.load(f) except filelock.FileLockException as e: raise return store def write_file(full_pathname, data): """ See http://david.dlma.com/blog/dads-project-in-the-garage """ try: with filelock.FileLock(full_pathname): with tempfile.NamedTemporaryFile(mode='w', dir=os.path.dirname(full_pathname), delete=False) as f: yaml.dump(data, f) # exiting this "with" flushes, but does not sync f.flush() os.fdatasync(f.fileno()) # Faster, Unix only tempname = f.name os.rename(tempname, full_pathname) # Atomic on Unix, not Windows except filelock.FileLockException as e: raise def map_ip_to_location(ip): """ Use a meaningful location instead of an IP address where possible.""" if ip == '67.188.28.83': return 'home' elif ip in ('50.224.7.233', '50.224.7.248'): return 'work' return ip device_prefixes = { '1G': '(Au)', '5S': '(Br)', 'YW': '(FW)', 'YY': '(Da)', '2N': '(Li)', '1R': '(Ty)', 'YU': '(Lf)', 'YP': '(C4)', } def map_key_to_prefix(k): """ Return a hint about the key if we can.""" try: return "%s " % device_prefixes[k[:2]] except KeyError: pass return "" def print_as_table(rows, underline_header = True): """ Prints rows with consistent column widths. """ cols = zip(*rows) # Compute column widths by taking maximum length of values per column col_widths = [max(len(value) for value in col) for col in cols] # Create a suitable format string for nice table output fmt = "\t".join(["{:<%d}" % i for i in col_widths]) # Print each row using the computed format if underline_header: print fmt.format(*rows.pop(0)) print "\t".join([('-' * i) for i in col_widths]) # column header separator for row in rows: print fmt.format(*row) def print_footer(): print print 'Tip:' print 'sync_ip () { export ENV_VAR=$(curl -s "%s?k=YP008G581095,2N00FG504225"); }' % os.environ['SCRIPT_URI'] print 'curl --data "`hostname -s`=`hostname -i`&auth=$AUTH" "%s"' % os.environ['SCRIPT_URI'] print 'Source code at http://git.dlma.com/kvs.git/' if __name__ == '__main__': localdir = os.path.abspath(os.path.dirname(sys.argv[0])) args = cgi.FieldStorage() key = None auth = None get_key = False auth_in_get = False doing_post = False # Find the parameters that are GET parameters # Could also check os.environ['REQUEST_METHOD'] == 'POST', # but that doesn't say which way individual params were sent. if os.environ['QUERY_STRING']: qs = os.environ['QUERY_STRING'].split('&') for pair in qs: k, v = pair.split('=') if k == 'k': get_key = True if k == 'auth': auth_in_get = True # Could be either GET or POST. Need to check QUERY_STRING above. if "k" in args: key = args["k"].value if os.environ['REQUEST_METHOD'] == 'POST': doing_post = True if not auth_in_get and "auth" in args: auth = args["auth"].value store = read_file(os.path.join(localdir, store_name)) if get_key: keys = key.split(',') matches = sorted([(store[x]['time'], store[x]['value']) for x in keys if x in store], reverse=True) if matches: print "Content-type: text/plain; charset=utf-8\n" sys.stdout.write(matches[0][1]) else: print 'Status: 404 Not Found\n' else: wrote_response = False with open('auth.txt', 'r') as f: authorization = f.read().strip() if doing_post and auth == authorization: print "Content-type: text/plain; charset=utf-8\n" for new_key in args: if new_key =='auth': continue if len(new_key) > 80 or len(args[new_key].value) > 140: print "Error: A key or value was too long." sys.exit(0) if new_key in store and 'created' in store[new_key]: created_time = store[new_key]['created'] else: created_time = int(time.time()) store[new_key] = {'value': args[new_key].value, 'time': int(time.time()), 'created': created_time, 'origin': os.environ['REMOTE_ADDR']} write_file(os.path.join(localdir, store_name), store) wrote_response = True print "OK" elif not os.environ['QUERY_STRING'] and not doing_post: # just the home page, then print "Content-type: text/plain; charset=UTF-8\n" wrote_response = True rows = [('key', 'value', 'created', 'updated', 'origin', 'notes'),] for key in sorted(store, key=lambda k: store[k]['time'], reverse=True): d = store[key] td = texttime.stringify(datetime.timedelta(seconds=time.time()-d['time'])) created = time.strftime('%Y-%m-%d', time.localtime(d['created'])) origin = map_ip_to_location(d['origin']) rows.append((key, d['value'], created, str(d['time']), origin, "%supdated %s ago" % (map_key_to_prefix(key), td))) print_as_table(rows) print_footer() if not wrote_response: print 'Status: 400 Bad Request\n'