commit 2e615a2f697cfa650e42173aab6220585a73e076 Author: Denis V. Dedkov Date: Tue Apr 18 15:12:22 2023 +0200 Initial commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c353379 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3-alpine + +RUN pip install websockets + +WORKDIR /beerlog-srv +COPY beerlog-srv.py . +COPY storage.py . +COPY users.obj . + +ARG BEERLOG_PORT +ENV BEERLOG_PORT $BEERLOG_PORT +EXPOSE $BEERLOG_PORT +CMD ["python", "./beerlog-srv.py"] + diff --git a/beerlog-srv.py b/beerlog-srv.py new file mode 100644 index 0000000..31ff397 --- /dev/null +++ b/beerlog-srv.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +import asyncio +import websockets +import json +import sys +import os +from storage import Storage + +CONNECTIONS = set() +STORAGES = { "users": Storage("users") } + +class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol): + async def check_credentials(self, username, password): + all_users = storage("users").read() + self.user = all_users[username] if username in all_users else None + return self.user != None + +def storage(name): + if not name in STORAGES: + STORAGES[name] = Storage(name) + return STORAGES[name] + +def _get_entity(event): + return event.get("entity", None) + +def _get_data(event): + return event.get("data", {}) + +async def add_entity(websocket, event): + name = _get_entity(event) + data = _get_data(event) + if name and data: + storage(name).create(data) + await broadcast(websocket, { "entity": name, "event": "created", "data": data }) + +async def get_entity(websocket, event): + name = _get_entity(event) + ts = _get_data(event).get("ts", 0) + data = storage(name).read(ts) + await websocket.send(json.dumps({ "entity": name, "event": "received", "data": data })) + +async def mod_entity(websocket, event): + name = _get_entity(event) + data = _get_data(event) + if name and data: + storage(name).update(data) + await broadcast(websocket, { "entity": name, "event": "modified", "data": data }) + +async def del_entity(websocket, event): + name = _get_entity(event) + entity_id = _get_data(event).get("id", None) + if storage(name).delete(entity_id): + await broadcast(websocket, { "entity": name, "event": "deleted", "data": entity_id }) + +async def describe(websocket, event): + connections = list(CONNECTIONS) + data = { c.user["id"]: connections.count(c) for c in connections } + await websocket.send(json.dumps({ "entity": "connections", "event": "described", "data": data })) + +async def broadcast(websocket, event): + websockets.broadcast(CONNECTIONS, json.dumps(event)) + +COMMANDS = { + "add": add_entity, + "get": get_entity, + "mod": mod_entity, + "del": del_entity, + "describe": describe + } + +async def handle(websocket): + try: + CONNECTIONS.add(websocket) + await broadcast(websocket, { "event": "connected", "entity": "users", "data": websocket.user}) + + async for message in websocket: + event = json.loads(message) + print(event, file=sys.stderr) + handler = COMMANDS.get(event["action"], broadcast) + await handler(websocket, event) + + finally: + CONNECTIONS.remove(websocket) + await broadcast(websocket, { "event": "disconnected", "entity": "users", "data": websocket.user}) + +async def main(): + port = os.environ.get("BEERLOG_PORT", 8000) + host = os.environ.get("BEERLOG_HOST", "0.0.0.0") + print(f"Start on {host}:{port}", file=sys.stderr) + async with websockets.serve(handle, host, port, create_protocol=UserInfoProtocol): + await asyncio.Future() + +asyncio.run(main()) + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..70f7719 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: "3.7" + +services: + beerlog-srv-dev: + container_name: beerlog-srv-dev + restart: always + build: + context: ./ + args: + BEERLOG_PORT: 8000 + network: host + ports: + - 8000:8000 + beerlog-srv-prod: + container_name: beerlog-srv-prod + restart: always + build: + context: ./ + args: + BEERLOG_PORT: 8080 + network: host + ports: + - 8080:8080 + diff --git a/storage.py b/storage.py new file mode 100644 index 0000000..c6855c9 --- /dev/null +++ b/storage.py @@ -0,0 +1,45 @@ +import os +import pickle +import uuid +import time + +class Storage(): + objects = {} + + def __init__(self, name): + self.name = f"./{name}.obj" + self._load() + + def _load(self): + if not os.path.exists(self.name): + self._save() + + with open(self.name, "rb") as s: + self.objects = pickle.load(s) + + def _save(self): + with open(self.name, "wb") as s: + pickle.dump(self.objects, s, pickle.HIGHEST_PROTOCOL) + + def create(self, obj): + obj["id"] = uuid.uuid1().hex + self.update(obj) + + def read(self, ts = 0): + if ts == 0: + return self.objects + else: + return { k: v for k, v in self.objects.items() if v["ts"] > ts } + + def update(self, obj): + obj["ts"] = time.time() + self.objects[obj["id"]] = obj + self._save() + + def delete(self, obj_id): + if not obj_id in self.objects: + return False + + del self.objects[obj_id] + self._save() + return True diff --git a/users.obj b/users.obj new file mode 100644 index 0000000..ae8474e Binary files /dev/null and b/users.obj differ