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 msgpack.exceptions import OutOfData
|
||||||
from typing import Callable, Self, Union
|
from typing import Callable, Self, Union
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
from os import linesep
|
||||||
|
|
||||||
def init_bulletins(root: PersistentMapping):
|
def init_bulletins(root: PersistentMapping):
|
||||||
if 'bulletins' not in root:
|
if 'bulletins' not in root:
|
||||||
@@ -66,6 +66,9 @@ class Server:
|
|||||||
if 'SYSTEM' not in conn.root.users:
|
if 'SYSTEM' not in conn.root.users:
|
||||||
logging.debug("Creating system user for first time.")
|
logging.debug("Creating system user for first time.")
|
||||||
User('SYSTEM', hidden=True, enabled=False).write_new(conn.root())
|
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())
|
init_bulletins(conn.root())
|
||||||
self.app = pe.app.Application()
|
self.app = pe.app.Application()
|
||||||
PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x))
|
PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x))
|
||||||
@@ -170,6 +173,12 @@ class Server:
|
|||||||
self.zeo_stop = stop
|
self.zeo_stop = stop
|
||||||
self.db = ZEO.DB(self.zeo_addr)
|
self.db = ZEO.DB(self.zeo_addr)
|
||||||
logging.info(f"Starting ZEO server with address {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.start(self.pe_server, self.pe_port)
|
||||||
self.app.register_callsigns(self.callsign)
|
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 packetserver.common import Message, Request, Response, PacketServerConnection, send_response, send_blank_response
|
||||||
from .bulletin import bulletin_root_handler
|
from .bulletin import bulletin_root_handler
|
||||||
from .users import user_root_handler, user_authorized
|
from .users import user_root_handler, user_authorized
|
||||||
|
from .objects import object_root_handler
|
||||||
import logging
|
import logging
|
||||||
from typing import Union
|
from typing import Union
|
||||||
import ZODB
|
import ZODB
|
||||||
@@ -46,7 +47,8 @@ def root_root_handler(req: Request, conn: PacketServerConnection,
|
|||||||
standard_handlers = {
|
standard_handlers = {
|
||||||
"": root_root_handler,
|
"": root_root_handler,
|
||||||
"bulletin": bulletin_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 ax25
|
||||||
import persistent
|
import persistent
|
||||||
import persistent.list
|
import persistent.list
|
||||||
|
from persistent.list import PersistentList
|
||||||
from persistent.mapping import PersistentMapping
|
from persistent.mapping import PersistentMapping
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Self,Union,Optional
|
from typing import Self,Union,Optional
|
||||||
@@ -12,6 +13,7 @@ import logging
|
|||||||
import uuid
|
import uuid
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from packetserver.common.util import email_valid
|
from packetserver.common.util import email_valid
|
||||||
|
from BTrees.OOBTree import TreeSet
|
||||||
|
|
||||||
class User(persistent.Persistent):
|
class User(persistent.Persistent):
|
||||||
def __init__(self, username: str, enabled: bool = True, hidden: bool = False, bio: str = "", status: str = "",
|
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.bio = bio
|
||||||
self._status = ""
|
self._status = ""
|
||||||
self.status = status
|
self.status = status
|
||||||
|
self._objects = TreeSet()
|
||||||
|
|
||||||
def write_new(self, db_root: PersistentMapping):
|
def write_new(self, db_root: PersistentMapping):
|
||||||
all_uuids = [db_root['users'][x].uuid for x in db_root['users']]
|
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']:
|
if self.username not in db_root['users']:
|
||||||
db_root['users'][self.username] = self
|
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
|
@property
|
||||||
def location(self) -> str:
|
def location(self) -> str:
|
||||||
return self._location
|
return self._location
|
||||||
|
|||||||
Reference in New Issue
Block a user