storage.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import asyncio
  2. import contextvars
  3. import json
  4. import threading
  5. import uuid
  6. from pathlib import Path
  7. from typing import Dict
  8. import aiofiles
  9. from fastapi import Request
  10. from starlette.middleware.base import BaseHTTPMiddleware
  11. request_contextvar = contextvars.ContextVar('request_var', default=None)
  12. class ReadOnlyDict:
  13. def __init__(self, data: Dict, write_error_message: str = 'Read-only dict'):
  14. self._data = data
  15. self._write_error_message = write_error_message
  16. def __getitem__(self, item):
  17. return self._data[item]
  18. def __iter__(self):
  19. return iter(self._data)
  20. def __len__(self):
  21. return len(self._data)
  22. def __setitem__(self, key, value):
  23. raise TypeError(self._write_error_message)
  24. class PersistentDict(dict):
  25. def __init__(self, filename: Path, *arg, **kw):
  26. self.filename = filename
  27. self.lock = threading.Lock()
  28. self.load()
  29. self.update(*arg, **kw)
  30. self.modified = bool(arg or kw)
  31. def load(self):
  32. with self.lock:
  33. if self.filename.exists():
  34. with open(self.filename, 'r') as f:
  35. try:
  36. self.update(json.load(f))
  37. except json.JSONDecodeError:
  38. pass
  39. def __setitem__(self, key, value):
  40. with self.lock:
  41. super().__setitem__(key, value)
  42. self.modified = True
  43. def __delitem__(self, key):
  44. with self.lock:
  45. super().__delitem__(key)
  46. self.modified = True
  47. def clear(self):
  48. with self.lock:
  49. super().clear()
  50. self.modified = True
  51. async def backup(self):
  52. data = dict(self)
  53. if self.modified:
  54. async with aiofiles.open(self.filename, 'w') as f:
  55. await f.write(json.dumps(data))
  56. class RequestTrackingMiddleware(BaseHTTPMiddleware):
  57. async def dispatch(self, request: Request, call_next):
  58. request_contextvar.set(request)
  59. if 'id' not in request.session:
  60. request.session['id'] = str(uuid.uuid4())
  61. request.state.responded = False
  62. response = await call_next(request)
  63. request.state.responded = True
  64. return response
  65. class Storage:
  66. def __init__(self):
  67. self.storage_dir = Path('.nicegui')
  68. self.storage_dir.mkdir(exist_ok=True)
  69. self._general = PersistentDict(self.storage_dir / 'storage_general.json')
  70. self._individuals = PersistentDict(self.storage_dir / 'storage_individuals.json')
  71. @property
  72. def browser(self) -> Dict:
  73. """Small storage that is saved directly within the user's browser (encrypted cookie).
  74. The data is shared between all browser tab and can only be modified before the initial request has been submitted.
  75. Normally it is better to use `app.storage.individual` instead to reduce payload, improved security and larger storage capacity)."""
  76. request: Request = request_contextvar.get()
  77. if request is None:
  78. raise RuntimeError('storage.browser needs a storage_secret passed in ui.run()')
  79. if request.state.responded:
  80. return ReadOnlyDict(
  81. request.session,
  82. 'the response to the browser has already been build so modifications can not be send back anymore'
  83. )
  84. return request.session
  85. @property
  86. def individual(self) -> Dict:
  87. """Individual user storage that is persisted on the server (where NiceGUI is executed).
  88. The data is stored in a file on the server.
  89. It is shared between all browser tabs by identifying the user via session cookie id.
  90. """
  91. request: Request = request_contextvar.get()
  92. if request is None:
  93. raise RuntimeError('storage.individual needs a storage_secret passed in ui.run()')
  94. if request.session['id'] not in self._individuals:
  95. self._individuals[request.session['id']] = {}
  96. return self._individuals[request.session['id']]
  97. @property
  98. def general(self) -> Dict:
  99. """General storage shared between all users that is persisted on the server (where NiceGUI is executed)."""
  100. return self._general
  101. async def backup(self):
  102. await self._general.backup()
  103. await self._individuals.backup()
  104. async def _loop(self):
  105. while True:
  106. await self.backup()
  107. await asyncio.sleep(10)