From 2de4a5d5ae909b69ec528a793f61471f86333398 Mon Sep 17 00:00:00 2001 From: dave Date: Sat, 6 Jul 2019 13:49:16 -0700 Subject: [PATCH] web store sessions in the database --- Dockerfile | 11 ++++--- README.md | 14 +-------- photoapp/daemon.py | 5 ++- photoapp/dbsession.py | 73 +++++++++++++++++++++++++++++++++++++++++++ photoapp/dbutils.py | 4 +-- 5 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 photoapp/dbsession.py diff --git a/Dockerfile b/Dockerfile index e3ab07f..c215793 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic +FROM ubuntu:bionic AS frontend RUN apt-get update && \ apt-get install -y wget software-properties-common && \ @@ -13,20 +13,21 @@ RUN cd /tmp/code && \ npm install && \ ./node_modules/.bin/grunt -FROM ubuntu:disco +FROM ubuntu:disco AS app ADD . /tmp/code/ -COPY --from=0 /tmp/code/styles/dist/style.css /tmp/code/styles/dist/style.css - RUN apt-get update && \ apt-get install -y python3-pip +COPY --from=frontend /tmp/code/styles/dist/style.css /tmp/code/styles/dist/style.css + RUN pip3 install -U pip && \ cd /tmp/code && \ pip3 install -r requirements.txt && \ python3 setup.py install && \ - useradd --uid 1000 app + useradd --uid 1000 app && \ + rm -rf /tmp/code VOLUME /srv/library VOLUME /srv/cache diff --git a/README.md b/README.md index 205a1f7..9f51879 100644 --- a/README.md +++ b/README.md @@ -115,31 +115,19 @@ This would ingest all the files listed in `shas.txt` that aren't already in the Roadmap ------- - Stateless aka docker support - - ~~Photo storage~~ done - - ~~Abstract the storage api~~ done - - ~~Standardize on API ingest~~ done - - ~~Display and retrieval of images from the abstracted image store~~ done - - ~~Thumbnail gen~~ done - - ~~Database support~~ - - ~~Get the web UI code (daemon.py) using the same db access method as the api~~ - - ~~Support any connection URI sqlalchemy is happy with~~ - - ~~Tune certain databases if their uri is detected (sqlite and threads lol)~~ - - ~~Cache~~ done - - ~~Using the local fs seems fine?~~ done - Migration path - open database - copy files table to memory - recreate files table - insert into the new table, with replaced paths, generating a list of files moves at the same time - migrate files to the new storage according to the list -- Storage option for cherrypy sessions - Redis? - Flesh out CLI: - Config that is saved somewhere - Support additional fields on upload like title description tags etc - delete features - tag features - modify features (tags & images) - - Longer term ideas: - "fast ingest" method that touches the db/storage directly. This would scale better than the API ingest. - Dynamic svg placeholder for images we can't open + - Proactive thumbnail generation diff --git a/photoapp/daemon.py b/photoapp/daemon.py index 2529b65..dc6a7f5 100644 --- a/photoapp/daemon.py +++ b/photoapp/daemon.py @@ -6,6 +6,7 @@ from urllib.parse import urlparse from datetime import datetime, timedelta from photoapp.thumb import ThumbGenerator from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User +from photoapp.dbsession import DatabaseSession from photoapp.common import pwhash from photoapp.api import PhotosApi, LibraryManager from photoapp.dbutils import SAEnginePlugin, SATool, db, get_db_engine @@ -473,7 +474,8 @@ def main(): # Setup and mount API api = PhotosApi(library_manager) - cherrypy.tree.mount(api, '/api', {'/': {'tools.trailing_slash.on': False, + cherrypy.tree.mount(api, '/api', {'/': {'tools.sessions.on': False, + 'tools.trailing_slash.on': False, 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'photolib', 'tools.auth_basic.checkpassword': validate_password, @@ -481,6 +483,7 @@ def main(): # General config options cherrypy.config.update({ + 'tools.sessions.storage_class': DatabaseSession, 'tools.sessions.on': True, 'tools.sessions.locking': 'explicit', 'tools.sessions.timeout': 525600, diff --git a/photoapp/dbsession.py b/photoapp/dbsession.py new file mode 100644 index 0000000..02f93bb --- /dev/null +++ b/photoapp/dbsession.py @@ -0,0 +1,73 @@ +import cherrypy +from photoapp.types import Base +from sqlalchemy import Column, String, DateTime, Boolean, BLOB + +try: + import cPickle as pickle +except ImportError: + import pickle + + +class Session(Base): + __tablename__ = 'sessions' + session_id = Column(String(128), nullable=False, primary_key=True) + # user_id = Column(Integer, ForeignKey('users.user_id')) + expiration = Column(DateTime, nullable=False) + data = Column(BLOB) + is_valid = Column(Boolean, default=True, nullable=False) + + +class DatabaseSession(cherrypy.lib.sessions.Session): + """ + Sqlalchemy-backed backed for session storage. Note that we don't implement any of the locking methods because the + underlying database connection is being uses transaction anyway, which would prevent concurrent updates. + """ + @classmethod + def setup(cls, **kwargs): + for k, v in kwargs.items(): + setattr(cls, k, v) + + cls.cached = None + + def _exists(self): + if not self.cached: + self._load() + return bool(self.cached) + + def _load(self): + if not self.cached: + self.cached = cherrypy.request.db.query(Session).filter(Session.session_id == self.id). \ + with_for_update().first() + + if self.cached: + return pickle.loads(self.cached.data) + + return None + + def _save(self, expiration_time): + data = pickle.dumps( + (self._data, expiration_time), + pickle.HIGHEST_PROTOCOL) + + if not self.cached: + self.cached = Session(session_id=self.id, + expiration=expiration_time) + cherrypy.request.db.add(self.cached) + + self.cached.data = data + + def _delete(self): + if not self.cached: + self._load() + + if self.cached: + cherrypy.request.db.delete(self.cached) + + def acquire_lock(self): + pass + + def release_lock(self): + pass + + def clean_up(self): + pass #TODO diff --git a/photoapp/dbutils.py b/photoapp/dbutils.py index 9620f04..9951b05 100644 --- a/photoapp/dbutils.py +++ b/photoapp/dbutils.py @@ -67,7 +67,7 @@ class SATool(cherrypy.Tool): def __init__(self): cherrypy.Tool.__init__(self, 'before_request_body', self.bind_session, - priority=100) + priority=49) # slightly earlier than Sessions tool, which is 50 or 60 self.session = sqlalchemy.orm.scoped_session( sqlalchemy.orm.sessionmaker(autoflush=True, autocommit=False)) @@ -88,4 +88,4 @@ class SATool(cherrypy.Tool): self.session.rollback() raise finally: - self.session.remove() \ No newline at end of file + self.session.remove()