Adding object support. Not completed or tested at all.
This commit is contained in:
@@ -16,7 +16,7 @@ import time
|
||||
from msgpack.exceptions import OutOfData
|
||||
from typing import Callable, Self, Union
|
||||
from traceback import format_exc
|
||||
|
||||
from os import linesep
|
||||
|
||||
def init_bulletins(root: PersistentMapping):
|
||||
if 'bulletins' not in root:
|
||||
@@ -66,6 +66,9 @@ class Server:
|
||||
if 'SYSTEM' not in conn.root.users:
|
||||
logging.debug("Creating system user for first time.")
|
||||
User('SYSTEM', hidden=True, enabled=False).write_new(conn.root())
|
||||
if 'objects' not in conn.root():
|
||||
logging.debug("objects bucket missing, creating")
|
||||
conn.root.objects = OOBTree()
|
||||
init_bulletins(conn.root())
|
||||
self.app = pe.app.Application()
|
||||
PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x))
|
||||
@@ -170,6 +173,12 @@ class Server:
|
||||
self.zeo_stop = stop
|
||||
self.db = ZEO.DB(self.zeo_addr)
|
||||
logging.info(f"Starting ZEO server with address {self.zeo_addr}")
|
||||
try:
|
||||
zeo_address_file = str(self.home_dir.joinpath("zeo-address.txt"))
|
||||
open(zeo_address_file, 'w').write(f"{self.zeo_addr[0]}:{self.zeo_addr[1]}{linesep}")
|
||||
logging.info(f"Wrote ZEO server info to '{zeo_address_file}'")
|
||||
except:
|
||||
logging.warning(f"Couldn't write ZEO server info to '{zeo_address_file}'\n{format_exc()}")
|
||||
self.app.start(self.pe_server, self.pe_port)
|
||||
self.app.register_callsigns(self.callsign)
|
||||
|
||||
|
||||
20
src/packetserver/server/messages.py
Normal file
20
src/packetserver/server/messages.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""BBS private message system"""
|
||||
import ax25
|
||||
import persistent
|
||||
import persistent.list
|
||||
from persistent.mapping import PersistentMapping
|
||||
import datetime
|
||||
from typing import Self,Union,Optional
|
||||
from packetserver.common import PacketServerConnection, Request, Response, Message, send_response, send_blank_response
|
||||
import ZODB
|
||||
import logging
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
from packetserver.common.util import email_valid
|
||||
|
||||
# TODO all messages
|
||||
|
||||
class Attachment:
|
||||
"""Name and data that is sent with a message."""
|
||||
def __init__(self, name: str, ):
|
||||
pass
|
||||
316
src/packetserver/server/objects.py
Normal file
316
src/packetserver/server/objects.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""Server object storage system."""
|
||||
from copy import deepcopy
|
||||
|
||||
import persistent
|
||||
import ax25
|
||||
import persistent.list
|
||||
from persistent.mapping import PersistentMapping
|
||||
import datetime
|
||||
from typing import Self,Union,Optional
|
||||
from packetserver.common import PacketServerConnection, Request, Response, Message, send_response, send_blank_response
|
||||
import ZODB
|
||||
import logging
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
from packetserver.server.users import User, user_authorized
|
||||
from collections import namedtuple
|
||||
|
||||
class Object(persistent.Persistent):
|
||||
def __init__(self, name: str = "", data: Union[bytes,bytearray,str] = None):
|
||||
self.private = False
|
||||
self._binary = False
|
||||
self._data = b''
|
||||
self._name = ""
|
||||
self._owner = None
|
||||
if data:
|
||||
self.data = data
|
||||
if name:
|
||||
self._name = name
|
||||
self._uuid = None
|
||||
self.created_at = datetime.datetime.now(datetime.UTC)
|
||||
self.modified_at = datetime.datetime.now(datetime.UTC)
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name: str):
|
||||
if name.strip() != self._name:
|
||||
if len(name.strip()) > 300:
|
||||
raise ValueError("Object names must be no more than 300 characters.")
|
||||
self._name = name.strip()
|
||||
self.touch()
|
||||
|
||||
def touch(self):
|
||||
self.modified_at = datetime.datetime.now(datetime.UTC)
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return len(self.data)
|
||||
|
||||
@property
|
||||
def binary(self):
|
||||
return self._binary
|
||||
|
||||
@property
|
||||
def data(self) -> Union[str,bytes]:
|
||||
if self.binary:
|
||||
return self._data
|
||||
else:
|
||||
return self._data.decode()
|
||||
|
||||
@data.setter
|
||||
def data(self, data: Union[bytes,bytearray,str]):
|
||||
if type(data) in (bytes,bytearray):
|
||||
if bytes(data) != self._data:
|
||||
self._data = bytes(data)
|
||||
self._binary = True
|
||||
self.touch()
|
||||
else:
|
||||
if str(data).encode() != self._data:
|
||||
self._data = str(data).encode()
|
||||
self._binary = False
|
||||
self.touch()
|
||||
|
||||
@property
|
||||
def owner(self) -> Optional[UUID]:
|
||||
return self._owner
|
||||
|
||||
@owner.setter
|
||||
def owner(self, owner_uuid: UUID):
|
||||
if owner_uuid:
|
||||
if type(owner_uuid) is UUID:
|
||||
self._owner = owner_uuid
|
||||
self.touch()
|
||||
else:
|
||||
raise ValueError("Owner must be a UUID")
|
||||
else:
|
||||
self._owner = None
|
||||
self.touch()
|
||||
|
||||
def chown(self, username: str, db: ZODB.DB):
|
||||
un = username.strip().upper()
|
||||
old_owner_uuid = self._owner
|
||||
with db.transaction() as db:
|
||||
user = User.get_user_by_username(username, db.root())
|
||||
old_owner = User.get_user_by_uuid(old_owner_uuid, db.root())
|
||||
if user:
|
||||
db.root.objects[self.uuid].owner = user.uuid
|
||||
user.add_obj_uuid(self.uuid)
|
||||
if old_owner_uuid:
|
||||
if old_owner:
|
||||
old_owner.remove_obj_uuid(self.uuid)
|
||||
else:
|
||||
raise KeyError(f"User '{un}' not found.")
|
||||
|
||||
@classmethod
|
||||
def get_object_by_uuid(cls, obj: UUID, db_root: PersistentMapping):
|
||||
return db_root['objects'].get(obj)
|
||||
|
||||
@classmethod
|
||||
def get_objects_by_username(cls, username: str, db: ZODB.DB) -> list[Self]:
|
||||
un = username.strip().upper()
|
||||
objs = []
|
||||
with db.transaction() as db:
|
||||
user = User.get_user_by_username(username, db.root())
|
||||
if user:
|
||||
uuids = user.object_uuids
|
||||
for u in uuids:
|
||||
try:
|
||||
obj = cls.get_object_by_uuid(u, db)
|
||||
if obj:
|
||||
objs.append(obj)
|
||||
except:
|
||||
pass
|
||||
return objs
|
||||
|
||||
@property
|
||||
def uuid(self) -> Optional[UUID]:
|
||||
return self._uuid
|
||||
|
||||
def write_new(self, db: ZODB.DB) -> UUID:
|
||||
if self.uuid:
|
||||
raise KeyError("Object already has UUID. Manually clear it to write it again.")
|
||||
self._uuid = uuid.uuid4()
|
||||
with db.transaction() as db:
|
||||
while self.uuid in db.root.objects:
|
||||
self._uuid = uuid.uuid4()
|
||||
db.root.objects[self.uuid] = self
|
||||
self.touch()
|
||||
return self.uuid
|
||||
|
||||
def to_dict(self, include_data: bool = True) -> dict:
|
||||
data = b''
|
||||
if include_data:
|
||||
data = self.data
|
||||
return {
|
||||
"name": self.name,
|
||||
"uuid_bytes": self.uuid.bytes,
|
||||
"size_bytes": self.size,
|
||||
"binary": self.binary,
|
||||
"private": self.private,
|
||||
"created_at": self.created_at,
|
||||
"modified_at": self.modified_at,
|
||||
"includes_data": include_data,
|
||||
"data": data
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: dict) -> Self:
|
||||
o = Object(name=obj['name'])
|
||||
o._uuid = UUID(bytes=obj['uuid_bytes'])
|
||||
o.private = obj['private']
|
||||
o.data = obj['data']
|
||||
o._binary = obj['binary']
|
||||
return o
|
||||
|
||||
def authorized_write(self, username: str, db: ZODB.DB):
|
||||
un = username.strip().upper()
|
||||
with db.transaction() as db:
|
||||
user = User.get_user_by_username(username, db.root())
|
||||
if user:
|
||||
if user.uuid == self.owner:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def authorized_get(self, username: str, db: ZODB.DB):
|
||||
if not self.private:
|
||||
return True
|
||||
un = username.strip().upper()
|
||||
with db.transaction() as db:
|
||||
user = User.get_user_by_username(username, db.root())
|
||||
if user:
|
||||
if user.uuid == self.owner:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
DisplayOptions = namedtuple('DisplayOptions', ['get_data', 'limit', 'sort_by', 'reverse', 'search'])
|
||||
|
||||
def parse_display_options(req: Request) -> DisplayOptions:
|
||||
limit = req.vars.get('limit')
|
||||
try:
|
||||
limit = int(limit)
|
||||
except:
|
||||
limit = None
|
||||
|
||||
d = req.vars.get('fetch')
|
||||
if type(d) is str:
|
||||
d.lower().strip()
|
||||
if d in [1, 'y', True, 'yes', 'true', 't']:
|
||||
get_data = True
|
||||
else:
|
||||
get_data = False
|
||||
|
||||
r = req.vars.get('reverse')
|
||||
if type(r) is str:
|
||||
r.lower().strip()
|
||||
if r in [1, 'y', True, 'yes', 'true', 't']:
|
||||
reverse = True
|
||||
else:
|
||||
reverse = False
|
||||
|
||||
sort = req.vars.get('sort')
|
||||
sort_by = "name"
|
||||
if type(sort) is str:
|
||||
sort = sort.lower().strip()
|
||||
if sort == "date":
|
||||
sort_by = "date"
|
||||
elif sort == "size":
|
||||
sort_by = "size"
|
||||
|
||||
s = req.vars.get('search')
|
||||
search = None
|
||||
if type(s) is str:
|
||||
s = s.lower()
|
||||
if s:
|
||||
search = str(s)
|
||||
|
||||
return DisplayOptions(get_data, limit, sort_by, reverse, search)
|
||||
|
||||
def object_display_filter(source: list[Object], opts: DisplayOptions) -> list[dict]:
|
||||
if opts.search:
|
||||
objs = [x for x in source if str(opts.search) in x.name.lower()]
|
||||
else:
|
||||
objs = deepcopy(source)
|
||||
|
||||
if opts.sort_by == "size":
|
||||
objs.sort(key=lambda x: x.size, reverse=opts.reverse)
|
||||
|
||||
elif opts.sort_by == "date":
|
||||
objs.sort(key=lambda x: x.modified_at, reverse=opts.reverse)
|
||||
else:
|
||||
objs.sort(key=lambda x: x.name, reverse=opts.reverse)
|
||||
|
||||
if opts.limit:
|
||||
if len(objs) >= opts.limit:
|
||||
objs = objs[:opts.limit]
|
||||
|
||||
return [o.to_dict(include_data=opts.get_data) for o in objs]
|
||||
|
||||
def handle_get_no_path(req: Request, conn: PacketServerConnection, db: ZODB.DB):
|
||||
opts = parse_display_options(req)
|
||||
response = Response.blank()
|
||||
response.status_code = 404
|
||||
username = ax25.Address(conn.remote_callsign).call.upper().strip()
|
||||
with db.transaction() as db:
|
||||
user = User.get_user_by_username(username, db.root())
|
||||
if not user:
|
||||
send_blank_response(conn, req, status_code=500, payload="Unknown user account problem")
|
||||
return
|
||||
if 'uuid' in req.vars:
|
||||
uid = req.vars['uuid']
|
||||
if type(uid) is bytes:
|
||||
obj = Object.get_object_by_uuid(UUID(bytes=uid), db.root())
|
||||
if obj:
|
||||
if not obj.owner == user.uuid:
|
||||
if not obj.private:
|
||||
send_blank_response(conn, req, status_code=401)
|
||||
return
|
||||
if opts.get_data:
|
||||
response.payload = obj.to_dict()
|
||||
response.status_code = 200
|
||||
else:
|
||||
response.payload = obj.to_dict(include_data=False)
|
||||
response.status_code = 200
|
||||
else:
|
||||
uuids = User.object_uuids
|
||||
objs = []
|
||||
for i in uuids:
|
||||
obj = Object.get_object_by_uuid(i, db.root())
|
||||
if not obj.private:
|
||||
objs.append(obj)
|
||||
else:
|
||||
if obj.uuid == user.uuid:
|
||||
objs.append(obj)
|
||||
response.payload = object_display_filter(objs, opts)
|
||||
response.status_code = 200
|
||||
|
||||
send_response(conn, response, req)
|
||||
|
||||
def handle_object_get(req: Request, conn: PacketServerConnection, db: ZODB.DB):
|
||||
# Case: User searching their own objects -> list
|
||||
# or passes specific UUID as var -> Object
|
||||
handle_get_no_path(req, conn, db)
|
||||
|
||||
|
||||
def handle_object_post(req: Request, conn: PacketServerConnection, db: ZODB.DB):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def object_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB):
|
||||
logging.debug(f"{req} being processed by user_root_handler")
|
||||
if not user_authorized(conn, db):
|
||||
logging.debug(f"user {conn.remote_callsign} not authorized")
|
||||
send_blank_response(conn, req, status_code=401)
|
||||
return
|
||||
logging.debug("user is authorized")
|
||||
if req.method is Request.Method.GET:
|
||||
handle_object_get(req, conn, db)
|
||||
elif req.method is Request.Method.POST:
|
||||
handle_object_post(req, conn, db)
|
||||
else:
|
||||
send_blank_response(conn, req, status_code=404)
|
||||
@@ -4,6 +4,7 @@ from msgpack.exceptions import OutOfData
|
||||
from packetserver.common import Message, Request, Response, PacketServerConnection, send_response, send_blank_response
|
||||
from .bulletin import bulletin_root_handler
|
||||
from .users import user_root_handler, user_authorized
|
||||
from .objects import object_root_handler
|
||||
import logging
|
||||
from typing import Union
|
||||
import ZODB
|
||||
@@ -46,7 +47,8 @@ def root_root_handler(req: Request, conn: PacketServerConnection,
|
||||
standard_handlers = {
|
||||
"": root_root_handler,
|
||||
"bulletin": bulletin_root_handler,
|
||||
"user": user_root_handler
|
||||
"user": user_root_handler,
|
||||
"object": object_root_handler
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import ax25
|
||||
import persistent
|
||||
import persistent.list
|
||||
from persistent.list import PersistentList
|
||||
from persistent.mapping import PersistentMapping
|
||||
import datetime
|
||||
from typing import Self,Union,Optional
|
||||
@@ -12,6 +13,7 @@ import logging
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
from packetserver.common.util import email_valid
|
||||
from BTrees.OOBTree import TreeSet
|
||||
|
||||
class User(persistent.Persistent):
|
||||
def __init__(self, username: str, enabled: bool = True, hidden: bool = False, bio: str = "", status: str = "",
|
||||
@@ -33,6 +35,7 @@ class User(persistent.Persistent):
|
||||
self.bio = bio
|
||||
self._status = ""
|
||||
self.status = status
|
||||
self._objects = TreeSet()
|
||||
|
||||
def write_new(self, db_root: PersistentMapping):
|
||||
all_uuids = [db_root['users'][x].uuid for x in db_root['users']]
|
||||
@@ -43,6 +46,21 @@ class User(persistent.Persistent):
|
||||
if self.username not in db_root['users']:
|
||||
db_root['users'][self.username] = self
|
||||
|
||||
@property
|
||||
def object_uuids(self) -> list[UUID]:
|
||||
return list(self._objects)
|
||||
|
||||
def remove_obj_uuid(self, obj: UUID):
|
||||
self._objects.remove(obj)
|
||||
|
||||
def add_obj_uuid(self, obj: UUID):
|
||||
self._objects.add(obj)
|
||||
|
||||
def user_has_obj(self, obj: UUID) -> bool:
|
||||
if obj in self._objects:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def location(self) -> str:
|
||||
return self._location
|
||||
|
||||
Reference in New Issue
Block a user