|
@@ -1,6 +1,12 @@
|
|
|
|
+import asyncio
|
|
import contextvars
|
|
import contextvars
|
|
|
|
+import json
|
|
|
|
+import threading
|
|
|
|
+import uuid
|
|
|
|
+from pathlib import Path
|
|
from typing import Dict
|
|
from typing import Dict
|
|
|
|
|
|
|
|
+import aiofiles
|
|
from fastapi import Request
|
|
from fastapi import Request
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
|
|
@@ -25,20 +31,63 @@ class ReadOnlyDict:
|
|
raise TypeError(self._write_error_message)
|
|
raise TypeError(self._write_error_message)
|
|
|
|
|
|
|
|
|
|
|
|
+class PersistentDict(dict):
|
|
|
|
+ def __init__(self, filename: Path, *arg, **kw):
|
|
|
|
+ self.filename = filename
|
|
|
|
+ self.lock = threading.Lock()
|
|
|
|
+ self.load()
|
|
|
|
+ self.update(*arg, **kw)
|
|
|
|
+
|
|
|
|
+ def load(self):
|
|
|
|
+ with self.lock:
|
|
|
|
+ if self.filename.exists():
|
|
|
|
+ with open(self.filename, 'r') as f:
|
|
|
|
+ try:
|
|
|
|
+ self.update(json.load(f))
|
|
|
|
+ except json.JSONDecodeError:
|
|
|
|
+ pass
|
|
|
|
+
|
|
|
|
+ def __setitem__(self, key, value):
|
|
|
|
+ with self.lock:
|
|
|
|
+ super().__setitem__(key, value)
|
|
|
|
+
|
|
|
|
+ def __delitem__(self, key):
|
|
|
|
+ with self.lock:
|
|
|
|
+ super().__delitem__(key)
|
|
|
|
+
|
|
|
|
+ async def backup(self):
|
|
|
|
+ data = dict(self)
|
|
|
|
+ if data:
|
|
|
|
+ async with aiofiles.open(self.filename, 'w') as f:
|
|
|
|
+ await f.write(json.dumps(data))
|
|
|
|
+
|
|
|
|
+
|
|
class RequestTrackingMiddleware(BaseHTTPMiddleware):
|
|
class RequestTrackingMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
async def dispatch(self, request: Request, call_next):
|
|
- token = request_contextvar.set(request)
|
|
|
|
|
|
+ if 'id' not in request.session:
|
|
|
|
+ request.session['id'] = str(uuid.uuid4())
|
|
request.state.responded = False
|
|
request.state.responded = False
|
|
|
|
+ token = request_contextvar.set(request)
|
|
response = await call_next(request)
|
|
response = await call_next(request)
|
|
- request.state.responded = True
|
|
|
|
request_contextvar.reset(token)
|
|
request_contextvar.reset(token)
|
|
|
|
+ request.state.responded = True
|
|
return response
|
|
return response
|
|
|
|
|
|
|
|
|
|
class Storage:
|
|
class Storage:
|
|
|
|
|
|
|
|
+ def __init__(self):
|
|
|
|
+ self.storage_dir = Path('.nicegui')
|
|
|
|
+ self.storage_dir.mkdir(exist_ok=True)
|
|
|
|
+ self._general = PersistentDict(self.storage_dir / 'storage_general.json')
|
|
|
|
+ self._individuals = PersistentDict(self.storage_dir / 'storage_individuals.json')
|
|
|
|
+
|
|
@property
|
|
@property
|
|
- def session(self) -> Dict:
|
|
|
|
|
|
+ def browser(self) -> Dict:
|
|
|
|
+ """Small storage that is saved directly within the user's browser (encrypted cookie).
|
|
|
|
+
|
|
|
|
+ The data is shared between all browser tab and can only be modified before the initial request has been submitted.
|
|
|
|
+ Normally it is better to use `app.storage.individual` instead to reduce payload, improved security and larger storage capacity)."""
|
|
request: Request = request_contextvar.get()
|
|
request: Request = request_contextvar.get()
|
|
if request.state.responded:
|
|
if request.state.responded:
|
|
return ReadOnlyDict(
|
|
return ReadOnlyDict(
|
|
@@ -46,3 +95,29 @@ class Storage:
|
|
'the response to the browser has already been build so modifications can not be send back anymore'
|
|
'the response to the browser has already been build so modifications can not be send back anymore'
|
|
)
|
|
)
|
|
return request.session
|
|
return request.session
|
|
|
|
+
|
|
|
|
+ @property
|
|
|
|
+ def individual(self) -> Dict:
|
|
|
|
+ """Individual user storage that is persisted on the server.
|
|
|
|
+
|
|
|
|
+ The data is stored in a file on the server.
|
|
|
|
+ It is shared between all browser tabs by identifying the user via session cookie id.
|
|
|
|
+ """
|
|
|
|
+ request: Request = request_contextvar.get()
|
|
|
|
+ if request.session['id'] not in self._individuals:
|
|
|
|
+ self._individuals[request.session['id']] = {}
|
|
|
|
+ return self._individuals[request.session['id']]
|
|
|
|
+
|
|
|
|
+ @property
|
|
|
|
+ def general(self) -> Dict:
|
|
|
|
+ """General storage shared between all users that is persisted on the server."""
|
|
|
|
+ return self._general
|
|
|
|
+
|
|
|
|
+ async def backup(self):
|
|
|
|
+ await self._general.backup()
|
|
|
|
+ await self._individuals.backup()
|
|
|
|
+
|
|
|
|
+ async def _loop(self):
|
|
|
|
+ while True:
|
|
|
|
+ await self.backup()
|
|
|
|
+ await asyncio.sleep(10)
|