api beginnings
This commit is contained in:
parent
3e6aae09db
commit
16271aa54a
41
photoapp/api.py
Normal file
41
photoapp/api.py
Normal file
@ -0,0 +1,41 @@
|
||||
import os
|
||||
import cherrypy
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from photoapp.library import PhotoLibrary
|
||||
from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy import func, and_, or_
|
||||
from photoapp.common import pwhash
|
||||
import math
|
||||
from urllib.parse import urlparse
|
||||
from photoapp.utils import mime2ext, auth, require_auth, photo_auth_filter, slugify
|
||||
from photoapp.dbutils import db
|
||||
|
||||
|
||||
class PhotosApi(object):
|
||||
def __init__(self):
|
||||
self.v1 = PhotosApiV1()
|
||||
|
||||
|
||||
class PhotosApiV1(object):
|
||||
def __init__(self):
|
||||
# self.tpl.filters.update(mime2ext=mime2ext,
|
||||
# basename=os.path.basename,
|
||||
# ceil=math.ceil,
|
||||
# statusstr=lambda x: str(x).split(".")[-1])
|
||||
pass
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
yield f"<plaintext>hello, this is the api. my database is: {db()}\n"
|
||||
|
||||
@cherrypy.expose
|
||||
def upload(self, files, meta):
|
||||
pass
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def findbysha(self, sha):
|
||||
pass
|
@ -8,58 +8,16 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy import func, and_, or_
|
||||
from photoapp.common import pwhash
|
||||
from photoapp.api import PhotosApi
|
||||
from photoapp.dbutils import SAEnginePlugin, SATool
|
||||
import math
|
||||
from urllib.parse import urlparse
|
||||
from photoapp.utils import mime2ext, auth, require_auth, photo_auth_filter, slugify
|
||||
|
||||
|
||||
APPROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
|
||||
|
||||
|
||||
def auth():
|
||||
"""
|
||||
Return the currently authorized username (per request) or None
|
||||
"""
|
||||
return cherrypy.session.get('authed', None)
|
||||
|
||||
|
||||
def mime2ext(mime):
|
||||
"""
|
||||
Given a mime type return the canonical file extension
|
||||
"""
|
||||
return {"image/png": "png",
|
||||
"image/jpeg": "jpg",
|
||||
"image/gif": "gif",
|
||||
"application/octet-stream-xmp": "xmp",
|
||||
"image/x-canon-cr2": "cr2",
|
||||
"video/mp4": "mp4",
|
||||
"video/quicktime": "mov"}[mime]
|
||||
|
||||
|
||||
def require_auth(func):
|
||||
"""
|
||||
Decorator: raise 403 unless session is authed
|
||||
"""
|
||||
def wrapped(*args, **kwargs):
|
||||
if not auth():
|
||||
raise cherrypy.HTTPError(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
def photo_auth_filter(query):
|
||||
"""
|
||||
Sqlalchemy helper: filter the given PhotoSet query to items that match the authorized user's PhotoStatus access
|
||||
level. Currently, authed users can access ALL photos, and unauthed users can access only PhotoStatus.public
|
||||
status items.
|
||||
"""
|
||||
return query.filter(PhotoSet.status == PhotoStatus.public) if not auth() else query
|
||||
|
||||
|
||||
def slugify(words):
|
||||
return ''.join(letter for letter in '-'.join(words.lower().split())
|
||||
if ('a' <= letter <= 'z') or ('0' <= letter <= '9') or letter == '-')
|
||||
|
||||
|
||||
class PhotosWeb(object):
|
||||
def __init__(self, library, template_dir):
|
||||
self.library = library
|
||||
@ -511,6 +469,13 @@ def main():
|
||||
'tools.auth_basic.realm': 'photolib',
|
||||
'tools.auth_basic.checkpassword': validate_password}})
|
||||
|
||||
cherrypy.tools.db = SATool()
|
||||
SAEnginePlugin(cherrypy.engine, library.engine).subscribe()
|
||||
api = PhotosApi()
|
||||
cherrypy.tree.mount(api, '/api', {'/': {'tools.trailing_slash.on': False,
|
||||
'tools.auth_basic.checkpassword': validate_password,
|
||||
'tools.db.on': True}})
|
||||
|
||||
cherrypy.config.update({
|
||||
'tools.sessions.on': True,
|
||||
'tools.sessions.locking': 'explicit',
|
||||
|
55
photoapp/dbutils.py
Normal file
55
photoapp/dbutils.py
Normal file
@ -0,0 +1,55 @@
|
||||
import sqlalchemy
|
||||
import cherrypy
|
||||
from cherrypy.process import plugins
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def db():
|
||||
return cherrypy.request.db
|
||||
|
||||
|
||||
class SAEnginePlugin(plugins.SimplePlugin):
|
||||
def __init__(self, bus, dbcon):
|
||||
plugins.SimplePlugin.__init__(self, bus)
|
||||
self.sa_engine = dbcon
|
||||
self.bus.subscribe("bind", self.bind)
|
||||
|
||||
def start(self):
|
||||
Base.metadata.create_all(self.sa_engine)
|
||||
|
||||
def bind(self, session):
|
||||
session.configure(bind=self.sa_engine)
|
||||
|
||||
|
||||
class SATool(cherrypy.Tool):
|
||||
def __init__(self):
|
||||
cherrypy.Tool.__init__(self, 'before_request_body',
|
||||
self.bind_session,
|
||||
priority=100)
|
||||
|
||||
self.session = sqlalchemy.orm.scoped_session(
|
||||
sqlalchemy.orm.sessionmaker(autoflush=True, autocommit=False))
|
||||
|
||||
def _setup(self):
|
||||
cherrypy.Tool._setup(self)
|
||||
cherrypy.request.hooks.attach('on_end_resource', self.commit_transaction, priority=80)
|
||||
|
||||
def bind_session(self):
|
||||
cherrypy.engine.publish('bind', self.session)
|
||||
cherrypy.request.db = self.session
|
||||
con = cherrypy.request.db.connection().connection.connection
|
||||
if hasattr(con, "ping"): # not available on sqlite
|
||||
con.ping()
|
||||
|
||||
def commit_transaction(self):
|
||||
cherrypy.request.db = None
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
self.session.rollback()
|
||||
raise
|
||||
finally:
|
||||
self.session.remove()
|
@ -122,7 +122,7 @@ def main():
|
||||
parser.add_argument("files", nargs="+")
|
||||
args = parser.parse_args()
|
||||
|
||||
library = PhotoLibrary("photos.db", "./library/")
|
||||
library = PhotoLibrary("photos.db", "./library", "./cache")
|
||||
|
||||
batch_ingest(library, args.files)
|
||||
|
||||
|
@ -1,15 +1,12 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Unicode, DECIMAL, ForeignKey, Boolean, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from photoapp.dbutils import Base
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import enum
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class PhotoStatus(enum.Enum):
|
||||
private = 0
|
||||
public = 1
|
||||
|
47
photoapp/utils.py
Normal file
47
photoapp/utils.py
Normal file
@ -0,0 +1,47 @@
|
||||
import cherrypy
|
||||
from photoapp.types import PhotoSet, PhotoStatus
|
||||
|
||||
|
||||
def mime2ext(mime):
|
||||
"""
|
||||
Given a mime type return the canonical file extension
|
||||
"""
|
||||
return {"image/png": "png",
|
||||
"image/jpeg": "jpg",
|
||||
"image/gif": "gif",
|
||||
"application/octet-stream-xmp": "xmp",
|
||||
"image/x-canon-cr2": "cr2",
|
||||
"video/mp4": "mp4",
|
||||
"video/quicktime": "mov"}[mime]
|
||||
|
||||
|
||||
def auth():
|
||||
"""
|
||||
Return the currently authorized username (per request) or None
|
||||
"""
|
||||
return cherrypy.session.get('authed', None)
|
||||
|
||||
|
||||
def require_auth(func):
|
||||
"""
|
||||
Decorator: raise 403 unless session is authed
|
||||
"""
|
||||
def wrapped(*args, **kwargs):
|
||||
if not auth():
|
||||
raise cherrypy.HTTPError(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
def photo_auth_filter(query):
|
||||
"""
|
||||
Sqlalchemy helper: filter the given PhotoSet query to items that match the authorized user's PhotoStatus access
|
||||
level. Currently, authed users can access ALL photos, and unauthed users can access only PhotoStatus.public
|
||||
status items.
|
||||
"""
|
||||
return query.filter(PhotoSet.status == PhotoStatus.public) if not auth() else query
|
||||
|
||||
|
||||
def slugify(words):
|
||||
return ''.join(letter for letter in '-'.join(words.lower().split())
|
||||
if ('a' <= letter <= 'z') or ('0' <= letter <= '9') or letter == '-')
|
@ -1,9 +1,9 @@
|
||||
backports.functools-lru-cache==1.5
|
||||
cheroot==6.5.2
|
||||
CherryPy==18.0.0
|
||||
CherryPy==18.1.1
|
||||
contextlib2==0.5.5
|
||||
jaraco.functools==1.20
|
||||
Jinja2==2.10
|
||||
Jinja2==2.10.1
|
||||
MarkupSafe==1.0
|
||||
more-itertools==4.3.0
|
||||
Pillow==5.2.0
|
||||
@ -11,6 +11,6 @@ portend==2.3
|
||||
python-magic==0.4.15
|
||||
pytz==2018.5
|
||||
six==1.11.0
|
||||
SQLAlchemy==1.2.11
|
||||
SQLAlchemy==1.3.5
|
||||
tempora==1.13
|
||||
zc.lockfile==1.3.0
|
||||
|
Loading…
Reference in New Issue
Block a user