home · contact · privacy
Add foreign key restraints, expand and fix tests, add deletion and forking.
[misc] / plomlib.py
1 import os
2 from http.server import BaseHTTPRequestHandler
3 from http.cookies import SimpleCookie
4 import json
5
6
7 class PlomException(Exception):
8     pass
9
10
11 class PlomDB:
12
13     def __init__(self, db_name):
14         self.db_file = db_name
15         self.lock_file = db_name+ '.lock'
16         if os.path.exists(self.db_file):
17             with open(self.db_file, 'r') as f:
18                 self.read_db_file(f)
19
20     def lock(self):
21         if os.path.exists(self.lock_file):
22             raise PlomException('Sorry, lock file!')
23         f = open(self.lock_file, 'w+')
24         f.close()
25
26     def unlock(self):
27         os.remove(self.lock_file)
28
29     def backup(self):
30         import shutil
31         from datetime import datetime, timedelta
32         if not os.path.exists(self.db_file):
33             return
34
35         # collect modification times of numbered .bak files
36         # print('DEBUG BACKUP')
37         bak_prefix = f'{self.db_file}.bak.'
38         # backup_dates = []
39         mtimes_to_paths = {}
40         for path in [path for path in os.listdir(os.path.dirname(bak_prefix))
41                      if path.startswith(os.path.basename(bak_prefix))]:
42             path = os.path.dirname(bak_prefix) + f'/{path}'
43             mod_time = os.path.getmtime(path)
44             # print(f'DEBUG pre-exists: {path} {mod_time}')
45             mtimes_to_paths[str(datetime.fromtimestamp(mod_time))] = path
46
47         # for mtime in sorted(mtimes_to_paths.keys()):
48         #     print(f'DEBUG mtimes_to_paths: {mtime}:{mtimes_to_paths[mtime]}')
49
50         # collect what numbered .bak files to save: the older, the fewer; for each
51         # timedelta, keep the newest file that's older
52         ages_to_keep = [timedelta(minutes=4**i) for i in range(0, 8)]
53         # print(f'DEBUG ages_to_keep: {ages_to_keep}')
54         now = datetime.now()
55         to_save = {}
56         for age in ages_to_keep:
57             limit = now - age
58             for mtime in reversed(sorted(mtimes_to_paths.keys())):
59                 # print(f'DEBUG checking if {mtime} < {limit} ({now} - {age})')
60                 if len(mtime) < 20:
61                     mtime_test = mtime + '.000000'
62                 else:
63                     mtime_test = mtime
64                 if datetime.strptime(mtime_test, '%Y-%m-%d %H:%M:%S.%f') < limit:
65                     # print('DEBUG it is, adding!')
66                     to_save[mtime] = mtimes_to_paths[mtime]
67                     break
68
69         for path in [path for path in mtimes_to_paths.values()
70                      if path not in to_save.values()]:
71             # print(f'DEBUG removing {path} cause not in to_save')
72             os.remove(path)
73
74         i = 0
75         for mtime in sorted(to_save.keys()):
76             source = to_save[mtime]
77             target = f'{bak_prefix}{i}'
78             # print(f'DEBUG to_save {source} -> {target}')
79             if source != target:
80                 shutil.move(source, target)
81             i += 1
82
83         # put copy of current state at end of bak list
84         # print(f'DEBUG saving current state to {bak_prefix}{i}')
85         shutil.copy(self.db_file, f'{bak_prefix}{i}')
86
87     def write_text_to_db(self, text, mode='w'):
88         self.lock()
89         self.backup()
90         with open(self.db_file, mode) as f:
91             f.write(text);
92         self.unlock()
93
94
95 class PlomHandler(BaseHTTPRequestHandler):
96     homepage = '/'
97     html_head = '<!DOCTYPE html>\n<html>\n<meta charset="UTF-8">'
98     html_foot = '</body>\n</html>'
99
100     def fail_400(self, e):
101         self.send_HTML(f'ERROR BAR: {e}', 400)
102
103     def send_HTML(self, html, code=200):
104         self.send_code_and_headers(code, [('Content-type', 'text/html')])
105         self.wfile.write(bytes(f'{self.html_head}\n{html}\n{self.html_foot}', 'utf-8'))
106
107     def ensure_cookies_to_set(self):
108         if not hasattr(self, 'cookies_to_set'):
109             self.cookies_to_set = []
110
111     def set_cookie(self, cookie_name, cookie_path, cookie_db):
112         self.ensure_cookies_to_set()
113         cookie = SimpleCookie()
114         cookie[cookie_name] = json.dumps(cookie_db)
115         cookie[cookie_name]['path'] = cookie_path
116         self.cookies_to_set += [cookie]
117
118     def unset_cookie(self, cookie_name, cookie_path):
119         self.ensure_cookies_to_set()
120         cookie = SimpleCookie()
121         cookie[cookie_name] = ''
122         cookie[cookie_name]['path'] = cookie_path
123         cookie[cookie_name]['expires'] = 'Thu, 01 Jan 1970 00:00:00 GMT'
124         self.cookies_to_set += [cookie]
125
126     def get_cookie_db(self, cookie_name):
127         cookie_db = {}
128         if 'Cookie' in self.headers:
129             cookie = SimpleCookie(self.headers['Cookie'])
130             if cookie_name in cookie:
131                 cookie_db = json.loads(cookie[cookie_name].value)
132         return cookie_db
133
134     def send_code_and_headers(self, code, headers=[]):
135         self.ensure_cookies_to_set()
136         self.send_response(code)
137         for cookie in self.cookies_to_set:
138             for morsel in cookie.values():
139                 self.send_header('Set-Cookie', morsel.OutputString())
140         for fieldname, content in headers:
141             self.send_header(fieldname, content)
142         self.end_headers()
143
144     def redirect(self, url='/'):
145         self.send_code_and_headers(302, [('Location', url)])
146
147     def try_do(self, do_method):
148         try:
149             do_method()
150         except PlomException as e:
151             self.fail_400(e)
152
153
154
155 def run_server(port, handler_class):
156     from http.server import HTTPServer
157     webServer = HTTPServer(('localhost', port), handler_class)
158     print(f"Server started http://localhost:{port}")
159     try:
160         webServer.serve_forever()
161     except KeyboardInterrupt:
162         pass
163     webServer.server_close()
164     print("Server stopped.")