2018-09-09 12:05:13 -07:00
|
|
|
import os
|
2019-07-04 18:41:57 -07:00
|
|
|
import math
|
2018-09-09 12:05:13 -07:00
|
|
|
import logging
|
2019-07-04 18:41:57 -07:00
|
|
|
import cherrypy
|
|
|
|
from urllib.parse import urlparse
|
2018-09-11 20:35:04 -07:00
|
|
|
from datetime import datetime, timedelta
|
2019-07-04 18:41:57 -07:00
|
|
|
from photoapp.thumb import ThumbGenerator
|
2018-09-23 16:39:46 -07:00
|
|
|
from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User
|
|
|
|
from photoapp.common import pwhash
|
2019-07-04 23:55:49 -07:00
|
|
|
from photoapp.api import PhotosApi, LibraryManager
|
2019-07-04 18:41:57 -07:00
|
|
|
from photoapp.dbutils import SAEnginePlugin, SATool, db, get_db_engine
|
|
|
|
from photoapp.utils import mime2ext, auth, require_auth, photoset_auth_filter, slugify
|
2019-07-04 23:55:49 -07:00
|
|
|
from photoapp.storage import uri_to_storage
|
2019-07-04 18:41:57 -07:00
|
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
|
|
from sqlalchemy import desc, func, and_, or_
|
2018-09-09 12:05:13 -07:00
|
|
|
|
|
|
|
|
2018-09-09 16:44:10 -07:00
|
|
|
APPROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
|
|
|
|
|
|
|
|
|
2019-07-04 18:41:57 -07:00
|
|
|
def validate_password(realm, username, password):
|
|
|
|
if db.query(User).filter(User.name == username, User.password == pwhash(password)).first():
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2018-09-09 12:05:13 -07:00
|
|
|
class PhotosWeb(object):
|
2019-07-04 18:41:57 -07:00
|
|
|
def __init__(self, library, thumbtool, template_dir):
|
2018-09-09 12:05:13 -07:00
|
|
|
self.library = library
|
2019-07-04 18:41:57 -07:00
|
|
|
self.thumbtool = thumbtool
|
2018-09-09 16:44:10 -07:00
|
|
|
self.tpl = Environment(loader=FileSystemLoader(template_dir),
|
2018-09-09 12:05:13 -07:00
|
|
|
autoescape=select_autoescape(['html', 'xml']))
|
2018-09-23 15:00:54 -07:00
|
|
|
self.tpl.filters.update(mime2ext=mime2ext,
|
|
|
|
basename=os.path.basename,
|
|
|
|
ceil=math.ceil,
|
|
|
|
statusstr=lambda x: str(x).split(".")[-1])
|
2018-09-22 15:12:01 -07:00
|
|
|
|
2018-09-09 12:05:13 -07:00
|
|
|
self.thumb = ThumbnailView(self)
|
|
|
|
self.photo = PhotoView(self)
|
|
|
|
self.download = DownloadView(self)
|
2018-09-11 20:35:04 -07:00
|
|
|
self.date = DateView(self)
|
2018-09-13 22:59:37 -07:00
|
|
|
self.tag = TagView(self)
|
|
|
|
self.album = self.tag
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2018-09-09 17:21:23 -07:00
|
|
|
def render(self, template, **kwargs):
|
2018-09-22 15:12:01 -07:00
|
|
|
"""
|
|
|
|
Render a template
|
|
|
|
"""
|
2018-09-09 17:21:23 -07:00
|
|
|
return self.tpl.get_template(template).render(**kwargs, **self.get_default_vars())
|
|
|
|
|
|
|
|
def get_default_vars(self):
|
2018-09-22 15:12:01 -07:00
|
|
|
"""
|
|
|
|
Return a dict containing variables expected to be on every page
|
|
|
|
"""
|
2018-09-22 18:01:33 -07:00
|
|
|
# all tags / albums with photos visible under the current auth context
|
2019-07-04 17:46:26 -07:00
|
|
|
tagq = db.query(Tag).join(TagItem).join(PhotoSet)
|
2018-09-22 18:01:33 -07:00
|
|
|
if not auth():
|
|
|
|
tagq = tagq.filter(PhotoSet.status == PhotoStatus.public)
|
2018-09-23 15:00:54 -07:00
|
|
|
tagq = tagq.filter(Tag.is_album == False).order_by(Tag.name).all() # pragma: manual auth
|
2018-09-22 18:01:33 -07:00
|
|
|
|
2019-07-04 17:46:26 -07:00
|
|
|
albumq = db.query(Tag).join(TagItem).join(PhotoSet)
|
2018-09-22 18:01:33 -07:00
|
|
|
if not auth():
|
|
|
|
albumq = albumq.filter(PhotoSet.status == PhotoStatus.public)
|
2018-09-23 15:00:54 -07:00
|
|
|
albumq = albumq.filter(Tag.is_album == True).order_by(Tag.name).all() # pragma: manual auth
|
2018-09-22 18:01:33 -07:00
|
|
|
|
2018-09-09 17:21:23 -07:00
|
|
|
ret = {
|
2018-09-22 18:01:33 -07:00
|
|
|
"all_tags": tagq,
|
|
|
|
"all_albums": albumq,
|
2018-09-22 15:12:23 -07:00
|
|
|
"path": cherrypy.request.path_info,
|
2018-09-22 18:01:33 -07:00
|
|
|
"auth": auth(),
|
|
|
|
"PhotoStatus": PhotoStatus
|
2018-09-09 17:21:23 -07:00
|
|
|
}
|
|
|
|
return ret
|
|
|
|
|
2018-09-09 12:05:13 -07:00
|
|
|
@cherrypy.expose
|
|
|
|
def index(self):
|
2018-09-22 15:12:01 -07:00
|
|
|
"""
|
|
|
|
Home page - redirect to the photo feed
|
|
|
|
"""
|
2018-09-09 12:05:13 -07:00
|
|
|
raise cherrypy.HTTPRedirect('feed', 302)
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
def feed(self, page=0, pgsize=25):
|
2018-09-22 15:12:01 -07:00
|
|
|
"""
|
|
|
|
/feed - main photo feed - show photos sorted by date, newest first
|
|
|
|
"""
|
2018-09-09 12:05:13 -07:00
|
|
|
page, pgsize = int(page), int(pgsize)
|
2019-07-04 18:41:57 -07:00
|
|
|
total_sets = photoset_auth_filter(db.query(func.count(PhotoSet.id))).first()[0]
|
|
|
|
images = photoset_auth_filter(db.query(PhotoSet)).order_by(PhotoSet.date.desc()). \
|
2018-09-22 18:01:33 -07:00
|
|
|
offset(pgsize * page).limit(pgsize).all()
|
2018-09-13 22:59:37 -07:00
|
|
|
yield self.render("feed.html", images=[i for i in images], page=page, pgsize=int(pgsize), total_sets=total_sets)
|
2018-09-09 12:05:13 -07:00
|
|
|
|
|
|
|
@cherrypy.expose
|
2018-09-22 15:12:01 -07:00
|
|
|
def stats(self):
|
|
|
|
"""
|
|
|
|
/stats - show server statistics
|
|
|
|
"""
|
2019-07-04 18:41:57 -07:00
|
|
|
images = photoset_auth_filter(db.query(func.count(PhotoSet.uuid),
|
|
|
|
func.strftime('%Y', PhotoSet.date).label('year'),
|
|
|
|
func.strftime('%m', PhotoSet.date).label('month'))). \
|
2018-09-10 21:19:02 -07:00
|
|
|
group_by('year', 'month').order_by(desc('year'), desc('month')).all()
|
2019-07-04 18:41:57 -07:00
|
|
|
tsize = photoset_auth_filter(db.query(func.sum(Photo.size)).join(PhotoSet)).scalar() # pragma: manual auth
|
2018-09-10 21:19:02 -07:00
|
|
|
yield self.render("monthly.html", images=images, tsize=tsize)
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2018-09-09 13:45:26 -07:00
|
|
|
@cherrypy.expose
|
2018-09-23 18:26:36 -07:00
|
|
|
def map(self, i=None, a=None, zoom=3):
|
2018-09-22 15:12:01 -07:00
|
|
|
"""
|
2018-09-23 18:26:36 -07:00
|
|
|
/map - show all photos on the a map. Passing $i will show a single photo, or passing $a will show photos under
|
|
|
|
the given tag.
|
2018-09-22 15:12:01 -07:00
|
|
|
TODO using so many coordinates is slow in the browser. dedupe them somehow.
|
|
|
|
"""
|
2019-07-04 18:41:57 -07:00
|
|
|
query = photoset_auth_filter(db.query(PhotoSet)).filter(PhotoSet.lat != 0, PhotoSet.lon != 0)
|
2018-09-23 18:26:36 -07:00
|
|
|
if a:
|
|
|
|
query = query.join(TagItem).join(Tag).filter(Tag.uuid == a)
|
2018-09-09 13:45:26 -07:00
|
|
|
if i:
|
|
|
|
query = query.filter(PhotoSet.uuid == i)
|
2018-09-09 17:21:23 -07:00
|
|
|
yield self.render("map.html", images=query.all(), zoom=int(zoom))
|
2018-09-09 13:45:26 -07:00
|
|
|
|
2018-09-13 22:59:22 -07:00
|
|
|
@cherrypy.expose
|
2018-09-22 18:01:33 -07:00
|
|
|
@require_auth
|
2018-09-22 15:12:23 -07:00
|
|
|
def create_tags(self, fromdate=None, uuid=None, tag=None, newtag=None, remove=None):
|
2018-09-13 22:59:22 -07:00
|
|
|
"""
|
2018-09-22 15:12:23 -07:00
|
|
|
/create_tags - tag multiple items selected by day of photo
|
|
|
|
:param fromdate: act upon photos taken on this day
|
|
|
|
:param uuid: act upon a single photo with this uuid
|
|
|
|
:param tag: target photos will have a tag specified by this uuid added
|
|
|
|
:param remove: target photos will have the tag specified by this uuid removed
|
|
|
|
:param newtag: new tag name to create
|
2018-09-13 22:59:22 -07:00
|
|
|
"""
|
2018-09-22 15:12:23 -07:00
|
|
|
|
|
|
|
def get_photos():
|
|
|
|
if fromdate:
|
|
|
|
dt = datetime.strptime(fromdate, "%Y-%m-%d")
|
|
|
|
dt_end = dt + timedelta(days=1)
|
2019-07-04 17:46:26 -07:00
|
|
|
photos = db.query(PhotoSet).filter(and_(PhotoSet.date >= dt,
|
2019-07-05 00:28:57 -07:00
|
|
|
PhotoSet.date < dt_end)).order_by(PhotoSet.date.desc())
|
2019-07-04 17:46:26 -07:00
|
|
|
num_photos = db.query(func.count(PhotoSet.id)). \
|
2019-07-05 00:28:57 -07:00
|
|
|
filter(and_(PhotoSet.date >= dt, PhotoSet.date < dt_end)).order_by(PhotoSet.date.desc()).scalar()
|
2018-09-22 15:12:23 -07:00
|
|
|
|
|
|
|
if uuid:
|
2019-07-04 17:46:26 -07:00
|
|
|
photos = db.query(PhotoSet).filter(PhotoSet.uuid == uuid)
|
|
|
|
num_photos = db.query(func.count(PhotoSet.id)).filter(PhotoSet.uuid == uuid).scalar()
|
2018-09-22 15:12:23 -07:00
|
|
|
return photos, num_photos
|
|
|
|
|
|
|
|
if remove:
|
2019-07-04 17:46:26 -07:00
|
|
|
rmtag = db.query(Tag).filter(Tag.uuid == remove).first()
|
2018-09-22 15:12:23 -07:00
|
|
|
photoq, _ = get_photos()
|
|
|
|
for photo in photoq:
|
2019-07-04 17:46:26 -07:00
|
|
|
db.query(TagItem).filter(TagItem.tag_id == rmtag.id, TagItem.set_id == photo.id).delete()
|
|
|
|
db.commit()
|
2018-09-13 22:59:22 -07:00
|
|
|
|
|
|
|
if newtag:
|
2019-07-04 17:46:26 -07:00
|
|
|
db.add(Tag(title=newtag.capitalize(), name=newtag, slug=slugify(newtag)))
|
|
|
|
db.commit()
|
2018-09-13 22:59:22 -07:00
|
|
|
|
2018-09-22 15:12:23 -07:00
|
|
|
photos, num_photos = get_photos()
|
|
|
|
|
2018-09-13 22:59:22 -07:00
|
|
|
if tag: # Create the tag on all the photos
|
2019-07-04 17:46:26 -07:00
|
|
|
tag = db.query(Tag).filter(Tag.uuid == tag).first()
|
2018-09-13 22:59:22 -07:00
|
|
|
for photo in photos.all():
|
2019-07-04 17:46:26 -07:00
|
|
|
if 0 == db.query(func.count(TagItem.id)).filter(TagItem.tag_id == tag.id,
|
|
|
|
TagItem.set_id == photo.id).scalar():
|
|
|
|
db.add(TagItem(tag_id=tag.id, set_id=photo.id))
|
|
|
|
db.commit()
|
2018-09-13 22:59:22 -07:00
|
|
|
|
2019-07-04 17:46:26 -07:00
|
|
|
alltags = db.query(Tag).order_by(Tag.name).all()
|
2018-09-22 15:12:23 -07:00
|
|
|
yield self.render("create_tags.html", images=photos, alltags=alltags,
|
|
|
|
num_photos=num_photos, fromdate=fromdate, uuid=uuid)
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
def login(self):
|
|
|
|
"""
|
|
|
|
/login - enable super features by logging into the app
|
|
|
|
"""
|
|
|
|
cherrypy.session['authed'] = cherrypy.request.login
|
2019-07-04 18:41:57 -07:00
|
|
|
print("Authed as", cherrypy.session['authed'])
|
2018-09-23 14:58:42 -07:00
|
|
|
dest = "/feed" if "Referer" not in cherrypy.request.headers \
|
|
|
|
else urlparse(cherrypy.request.headers["Referer"]).path
|
|
|
|
raise cherrypy.HTTPRedirect(dest, 302)
|
2018-09-22 15:12:23 -07:00
|
|
|
|
|
|
|
@cherrypy.expose
|
2018-09-22 18:01:33 -07:00
|
|
|
def logout(self):
|
2018-09-22 15:12:23 -07:00
|
|
|
"""
|
2018-09-23 14:58:42 -07:00
|
|
|
/logout
|
2018-09-22 15:12:23 -07:00
|
|
|
"""
|
2018-09-22 18:01:33 -07:00
|
|
|
cherrypy.session.clear()
|
2018-09-23 14:58:42 -07:00
|
|
|
dest = "/feed" if "Referer" not in cherrypy.request.headers \
|
|
|
|
else urlparse(cherrypy.request.headers["Referer"]).path
|
|
|
|
raise cherrypy.HTTPRedirect(dest, 302)
|
2018-09-13 22:59:22 -07:00
|
|
|
|
2018-09-23 15:26:41 -07:00
|
|
|
@cherrypy.expose
|
|
|
|
def error(self, status, message, traceback, version):
|
|
|
|
yield self.render("error.html", status=status, message=message, traceback=traceback)
|
|
|
|
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2018-09-11 20:35:04 -07:00
|
|
|
@cherrypy.popargs('date')
|
|
|
|
class DateView(object):
|
2018-09-13 22:59:22 -07:00
|
|
|
"""
|
|
|
|
View all the photos shot on a given date
|
|
|
|
"""
|
2018-09-11 20:35:04 -07:00
|
|
|
def __init__(self, master):
|
|
|
|
self.master = master
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
def index(self, date=None, page=0):
|
|
|
|
if date:
|
|
|
|
page = int(page)
|
|
|
|
pgsize = 100
|
|
|
|
dt = datetime.strptime(date, "%Y-%m-%d")
|
|
|
|
dt_end = dt + timedelta(days=1)
|
2019-07-04 18:41:57 -07:00
|
|
|
total_sets = photoset_auth_filter(db.query(func.count(PhotoSet.id))). \
|
2018-09-11 20:35:04 -07:00
|
|
|
filter(and_(PhotoSet.date >= dt, PhotoSet.date < dt_end)).first()[0]
|
2019-07-05 00:28:57 -07:00
|
|
|
images = photoset_auth_filter(db.query(PhotoSet)). \
|
|
|
|
filter(and_(PhotoSet.date >= dt, PhotoSet.date < dt_end)). \
|
|
|
|
order_by(PhotoSet.date.desc()). \
|
2018-09-11 20:35:04 -07:00
|
|
|
offset(page * pgsize).limit(pgsize).all()
|
2018-09-13 22:59:37 -07:00
|
|
|
yield self.master.render("date.html", page=page, pgsize=pgsize, total_sets=total_sets,
|
|
|
|
images=[i for i in images], date=dt)
|
2018-09-11 20:35:04 -07:00
|
|
|
return
|
2019-07-04 18:41:57 -07:00
|
|
|
images = photoset_auth_filter(db.query(PhotoSet, func.strftime('%Y-%m-%d',
|
|
|
|
PhotoSet.date).label('gdate'),
|
|
|
|
func.count('photos.id'),
|
|
|
|
func.strftime('%Y', PhotoSet.date).label('year'),
|
|
|
|
func.strftime('%m', PhotoSet.date).label('month'),
|
|
|
|
func.strftime('%d', PhotoSet.date).label('day'))). \
|
2018-09-11 20:35:04 -07:00
|
|
|
group_by('gdate').order_by(desc('year'), 'month', 'day').all()
|
|
|
|
yield self.master.render("dates.html", images=images)
|
|
|
|
|
|
|
|
|
2018-09-09 12:05:13 -07:00
|
|
|
@cherrypy.popargs('item_type', 'thumb_size', 'uuid')
|
|
|
|
class ThumbnailView(object):
|
2018-09-22 15:12:01 -07:00
|
|
|
"""
|
|
|
|
Generate and serve thumbnails on-demand
|
|
|
|
"""
|
2018-09-09 12:05:13 -07:00
|
|
|
def __init__(self, master):
|
|
|
|
self.master = master
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
def index(self, item_type, thumb_size, uuid):
|
|
|
|
uuid = uuid.split(".")[0]
|
2019-07-04 18:41:57 -07:00
|
|
|
query = photoset_auth_filter(db.query(Photo).join(PhotoSet))
|
2018-09-23 15:37:24 -07:00
|
|
|
|
|
|
|
query = query.filter(Photo.set.has(uuid=uuid)) if item_type == "set" \
|
|
|
|
else query.filter(Photo.uuid == uuid) if item_type == "one" \
|
2018-09-09 12:05:13 -07:00
|
|
|
else None
|
|
|
|
|
2018-09-22 18:01:33 -07:00
|
|
|
assert query
|
|
|
|
|
2018-09-09 12:05:13 -07:00
|
|
|
# prefer making thumbs from jpeg to avoid loading large raws
|
2018-09-23 18:26:36 -07:00
|
|
|
# jk we can't load raws anyway
|
2018-09-09 12:05:13 -07:00
|
|
|
first = None
|
|
|
|
best = None
|
|
|
|
for photo in query.all():
|
|
|
|
if first is None:
|
|
|
|
first = photo
|
|
|
|
if photo.format == "image/jpeg":
|
|
|
|
best = photo
|
|
|
|
break
|
|
|
|
thumb_from = best or first
|
|
|
|
if not thumb_from:
|
2018-09-23 15:37:24 -07:00
|
|
|
raise cherrypy.HTTPError(404)
|
2018-09-09 12:05:13 -07:00
|
|
|
# TODO some lock around calls to this based on uuid
|
2019-07-04 18:41:57 -07:00
|
|
|
thumb_path = self.master.thumbtool.make_thumb(thumb_from, thumb_size)
|
2018-09-09 12:05:13 -07:00
|
|
|
if thumb_path:
|
2018-09-09 13:45:26 -07:00
|
|
|
return cherrypy.lib.static.serve_file(thumb_path, "image/jpeg")
|
2018-09-09 12:05:13 -07:00
|
|
|
else:
|
2018-09-09 16:44:10 -07:00
|
|
|
return cherrypy.lib.static.serve_file(os.path.join(APPROOT, "styles/dist/unknown.svg"), "image/svg+xml")
|
2018-09-09 12:05:13 -07:00
|
|
|
|
|
|
|
|
|
|
|
@cherrypy.popargs('item_type', 'uuid')
|
|
|
|
class DownloadView(object):
|
2018-09-22 15:12:01 -07:00
|
|
|
"""
|
|
|
|
View original files or force-download them
|
|
|
|
"""
|
2018-09-09 12:05:13 -07:00
|
|
|
def __init__(self, master):
|
|
|
|
self.master = master
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
def index(self, item_type, uuid, preview=False):
|
|
|
|
uuid = uuid.split(".")[0]
|
|
|
|
query = None if item_type == "set" \
|
2019-07-04 18:41:57 -07:00
|
|
|
else photoset_auth_filter(db.query(Photo).join(PhotoSet)).filter(Photo.uuid == uuid) if item_type == "one" \
|
2018-09-09 12:05:13 -07:00
|
|
|
else None # TODO set download query
|
|
|
|
|
|
|
|
item = query.first()
|
2018-09-23 15:37:24 -07:00
|
|
|
if not item:
|
|
|
|
raise cherrypy.HTTPError(404)
|
2018-09-09 12:05:13 -07:00
|
|
|
extra = {}
|
|
|
|
if not preview:
|
|
|
|
extra.update(disposition="attachement", name=os.path.basename(item.path))
|
2019-07-04 18:41:57 -07:00
|
|
|
return cherrypy.lib.static.serve_fileobj(self.master.library.storage.open(item.path, 'rb'),
|
|
|
|
content_type=item.format, **extra)
|
2018-09-09 12:05:13 -07:00
|
|
|
|
|
|
|
|
|
|
|
@cherrypy.popargs('uuid')
|
|
|
|
class PhotoView(object):
|
2018-09-22 15:12:01 -07:00
|
|
|
"""
|
|
|
|
View a single photo
|
|
|
|
"""
|
2018-09-09 12:05:13 -07:00
|
|
|
def __init__(self, master):
|
|
|
|
self.master = master
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
def index(self, uuid):
|
2018-09-23 15:02:03 -07:00
|
|
|
# uuid = uuid.split(".")[0]
|
2019-07-04 18:41:57 -07:00
|
|
|
photo = photoset_auth_filter(db.query(PhotoSet)).filter(or_(PhotoSet.uuid == uuid,
|
|
|
|
PhotoSet.slug == uuid)).first()
|
2018-09-23 15:26:41 -07:00
|
|
|
if not photo:
|
|
|
|
raise cherrypy.HTTPError(404)
|
2018-09-09 17:21:23 -07:00
|
|
|
yield self.master.render("photo.html", image=photo)
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2018-09-22 18:01:33 -07:00
|
|
|
@cherrypy.expose
|
|
|
|
@require_auth
|
2018-09-23 15:02:03 -07:00
|
|
|
def op(self, uuid, op, title=None, description=None, offset=None):
|
2018-09-23 15:26:41 -07:00
|
|
|
"""
|
|
|
|
Modify a photo
|
|
|
|
:param op: operation to perform:
|
|
|
|
* "Make public":
|
|
|
|
* "Make private":
|
|
|
|
* "Save": update the photo's title, description, and date_offset fields
|
|
|
|
"""
|
2019-07-04 17:46:26 -07:00
|
|
|
photo = db.query(PhotoSet).filter(PhotoSet.uuid == uuid).first()
|
2018-09-22 18:01:33 -07:00
|
|
|
if op == "Make public":
|
|
|
|
photo.status = PhotoStatus.public
|
|
|
|
elif op == "Make private":
|
|
|
|
photo.status = PhotoStatus.private
|
2018-09-23 15:02:03 -07:00
|
|
|
elif op == "Save":
|
|
|
|
photo.title = title
|
|
|
|
photo.description = description
|
|
|
|
photo.slug = slugify(title) or None
|
|
|
|
photo.date_offset = int(offset) if offset else 0
|
2019-07-04 17:46:26 -07:00
|
|
|
db.commit()
|
2018-09-23 15:02:03 -07:00
|
|
|
raise cherrypy.HTTPRedirect('/photo/{}'.format(photo.slug or photo.uuid), 302)
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
@require_auth
|
|
|
|
def edit(self, uuid):
|
2019-07-04 18:41:57 -07:00
|
|
|
photo = photoset_auth_filter(db.query(PhotoSet)).filter(PhotoSet.uuid == uuid).first()
|
2018-09-23 15:02:03 -07:00
|
|
|
yield self.master.render("photo_edit.html", image=photo)
|
2018-09-22 18:01:33 -07:00
|
|
|
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2018-09-13 22:59:22 -07:00
|
|
|
@cherrypy.popargs('uuid')
|
|
|
|
class TagView(object):
|
|
|
|
"""
|
|
|
|
View the photos associated with a single tag
|
|
|
|
"""
|
|
|
|
def __init__(self, master):
|
|
|
|
self.master = master
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
def index(self, uuid, page=0):
|
|
|
|
page = int(page)
|
|
|
|
pgsize = 100
|
2018-09-22 15:12:23 -07:00
|
|
|
if uuid == "untagged":
|
2019-07-04 18:41:57 -07:00
|
|
|
numphotos = photoset_auth_filter(db.query(func.count(PhotoSet.id))). \
|
2019-07-04 17:46:26 -07:00
|
|
|
filter(~PhotoSet.id.in_(db.query(TagItem.set_id))).scalar()
|
2019-07-05 00:28:57 -07:00
|
|
|
photos = photoset_auth_filter(db.query(PhotoSet)).filter(~PhotoSet.id.in_(db.query(TagItem.set_id))). \
|
|
|
|
order_by(PhotoSet.date.desc()). \
|
2018-09-23 15:02:03 -07:00
|
|
|
offset(page * pgsize). \
|
|
|
|
limit(pgsize).all()
|
2018-09-22 15:12:23 -07:00
|
|
|
yield self.master.render("untagged.html", images=photos, total_items=numphotos, pgsize=pgsize, page=page)
|
|
|
|
else:
|
2019-07-04 17:46:26 -07:00
|
|
|
tag = db.query(Tag).filter(or_(Tag.uuid == uuid, Tag.slug == uuid)).first()
|
2019-07-04 18:41:57 -07:00
|
|
|
numphotos = photoset_auth_filter(db.query(func.count(Tag.id)).join(TagItem).join(PhotoSet)). \
|
2018-09-23 15:02:03 -07:00
|
|
|
filter(Tag.id == tag.id).scalar()
|
2019-07-04 18:41:57 -07:00
|
|
|
photos = photoset_auth_filter(db.query(PhotoSet)).join(TagItem).join(Tag). \
|
2018-09-23 15:02:03 -07:00
|
|
|
filter(Tag.id == tag.id). \
|
|
|
|
order_by(PhotoSet.date.desc()). \
|
|
|
|
offset(page * pgsize). \
|
|
|
|
limit(pgsize).all()
|
2018-09-22 15:12:23 -07:00
|
|
|
yield self.master.render("album.html", tag=tag, images=photos,
|
|
|
|
total_items=numphotos, pgsize=pgsize, page=page)
|
2018-09-13 22:59:22 -07:00
|
|
|
|
|
|
|
@cherrypy.expose
|
2018-09-22 18:01:33 -07:00
|
|
|
@require_auth
|
2018-09-23 18:26:36 -07:00
|
|
|
def op(self, uuid, op, title=None, description=None):
|
2018-09-23 13:04:50 -07:00
|
|
|
"""
|
|
|
|
Perform some action on this tag
|
|
|
|
- Promote: label this tag an album
|
|
|
|
- Demote: label this tag as only a tag and not an album
|
|
|
|
- Delete: remove this tag
|
|
|
|
- Make all public: mark all photos under this tag as public
|
|
|
|
- Make all private: mark all photos under this tag as private
|
|
|
|
"""
|
2019-07-04 17:46:26 -07:00
|
|
|
tag = db.query(Tag).filter(or_(Tag.uuid == uuid, Tag.slug == uuid)).first()
|
2018-09-13 22:59:22 -07:00
|
|
|
if op == "Demote to tag":
|
|
|
|
tag.is_album = 0
|
|
|
|
elif op == "Promote to album":
|
|
|
|
tag.is_album = 1
|
2018-09-22 15:12:35 -07:00
|
|
|
elif op == "Delete tag":
|
2019-07-04 17:46:26 -07:00
|
|
|
db.query(TagItem).filter(TagItem.tag_id == tag.id).delete()
|
|
|
|
db.delete(tag)
|
|
|
|
db.commit()
|
2018-09-22 15:12:35 -07:00
|
|
|
raise cherrypy.HTTPRedirect('/', 302)
|
2018-09-23 13:04:50 -07:00
|
|
|
elif op == "Make all public":
|
|
|
|
# TODO smarter query
|
2019-07-04 17:46:26 -07:00
|
|
|
for photo in db.query(PhotoSet).join(TagItem).join(Tag).filter(Tag.id == tag.id).all():
|
2018-09-23 13:04:50 -07:00
|
|
|
photo.status = PhotoStatus.public
|
|
|
|
elif op == "Make all private":
|
|
|
|
# TODO smarter query
|
2019-07-04 17:46:26 -07:00
|
|
|
for photo in db.query(PhotoSet).join(TagItem).join(Tag).filter(Tag.id == tag.id).all():
|
2018-09-23 13:04:50 -07:00
|
|
|
photo.status = PhotoStatus.private
|
2018-09-23 18:26:36 -07:00
|
|
|
elif op == "Save":
|
|
|
|
tag.title = title
|
|
|
|
tag.description = description
|
|
|
|
tag.slug = slugify(title)
|
2018-09-13 22:59:22 -07:00
|
|
|
else:
|
2018-09-22 15:12:35 -07:00
|
|
|
raise Exception("Invalid op: '{}'".format(op))
|
2019-07-04 17:46:26 -07:00
|
|
|
db.commit()
|
2018-09-23 18:26:36 -07:00
|
|
|
raise cherrypy.HTTPRedirect('/tag/{}'.format(tag.slug or tag.uuid), 302)
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
@require_auth
|
|
|
|
def edit(self, uuid):
|
2019-07-04 17:46:26 -07:00
|
|
|
tag = db.query(Tag).filter(Tag.uuid == uuid).first()
|
2018-09-23 18:26:36 -07:00
|
|
|
yield self.master.render("tag_edit.html", tag=tag)
|
2018-09-13 22:59:22 -07:00
|
|
|
|
|
|
|
|
2018-09-09 12:05:13 -07:00
|
|
|
def main():
|
|
|
|
import argparse
|
|
|
|
import signal
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Photod photo server")
|
|
|
|
|
|
|
|
parser.add_argument('-p', '--port', default=8080, type=int, help="tcp port to listen on")
|
|
|
|
parser.add_argument('-l', '--library', default="./library", help="library path")
|
2018-09-11 22:25:09 -07:00
|
|
|
parser.add_argument('-c', '--cache', default="./cache", help="cache path")
|
|
|
|
parser.add_argument('-s', '--database', default="./photos.db", help="path to persistent sqlite database")
|
2018-09-09 12:05:13 -07:00
|
|
|
parser.add_argument('--debug', action="store_true", help="enable development options")
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING,
|
|
|
|
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
|
|
|
|
|
2019-07-04 18:41:57 -07:00
|
|
|
# Get database connection
|
|
|
|
engine = get_db_engine(args.database)
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2019-07-04 18:41:57 -07:00
|
|
|
# Setup database in web framework
|
|
|
|
cherrypy.tools.db = SATool()
|
|
|
|
SAEnginePlugin(cherrypy.engine, engine).subscribe()
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2019-07-04 18:41:57 -07:00
|
|
|
# Create various internal tools
|
2019-07-04 23:55:49 -07:00
|
|
|
library_storage = uri_to_storage(args.library)
|
2019-07-04 18:41:57 -07:00
|
|
|
library_manager = LibraryManager(library_storage)
|
|
|
|
thumbnail_tool = ThumbGenerator(library_manager, args.cache)
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2019-07-04 18:41:57 -07:00
|
|
|
# Setup and mount web ui
|
|
|
|
tpl_dir = os.path.join(APPROOT, "templates") if not args.debug else "templates"
|
|
|
|
web = PhotosWeb(library_manager, thumbnail_tool, tpl_dir)
|
2018-09-23 18:26:36 -07:00
|
|
|
cherrypy.tree.mount(web, '/', {'/': {'tools.trailing_slash.on': False,
|
2019-07-04 17:46:26 -07:00
|
|
|
'tools.db.on': True,
|
2018-09-23 18:26:36 -07:00
|
|
|
'error_page.403': web.error,
|
|
|
|
'error_page.404': web.error},
|
2018-09-09 12:05:13 -07:00
|
|
|
'/static': {"tools.staticdir.on": True,
|
2018-09-22 15:12:23 -07:00
|
|
|
"tools.staticdir.dir": os.path.join(APPROOT, "styles/dist")
|
|
|
|
if not args.debug else os.path.abspath("styles/dist")},
|
|
|
|
'/login': {'tools.auth_basic.on': True,
|
|
|
|
'tools.auth_basic.realm': 'photolib',
|
|
|
|
'tools.auth_basic.checkpassword': validate_password}})
|
2018-09-09 12:05:13 -07:00
|
|
|
|
2019-07-04 18:41:57 -07:00
|
|
|
# Setup and mount API
|
2019-07-01 13:53:50 -07:00
|
|
|
api = PhotosApi(library_manager)
|
2019-06-17 22:43:57 -07:00
|
|
|
cherrypy.tree.mount(api, '/api', {'/': {'tools.trailing_slash.on': False,
|
2019-06-24 14:33:11 -07:00
|
|
|
'tools.auth_basic.on': True,
|
|
|
|
'tools.auth_basic.realm': 'photolib',
|
2019-06-17 22:43:57 -07:00
|
|
|
'tools.auth_basic.checkpassword': validate_password,
|
|
|
|
'tools.db.on': True}})
|
|
|
|
|
2019-07-04 18:41:57 -07:00
|
|
|
# General config options
|
2018-09-09 12:05:13 -07:00
|
|
|
cherrypy.config.update({
|
2018-09-22 15:12:23 -07:00
|
|
|
'tools.sessions.on': True,
|
|
|
|
'tools.sessions.locking': 'explicit',
|
|
|
|
'tools.sessions.timeout': 525600,
|
2018-09-09 12:05:13 -07:00
|
|
|
'request.show_tracebacks': True,
|
|
|
|
'server.socket_port': args.port,
|
|
|
|
'server.thread_pool': 25,
|
|
|
|
'server.socket_host': '0.0.0.0',
|
|
|
|
'server.show_tracebacks': True,
|
|
|
|
'log.screen': False,
|
|
|
|
'engine.autoreload.on': args.debug
|
|
|
|
})
|
|
|
|
|
2019-07-04 18:41:57 -07:00
|
|
|
# Setup signal handling and run it.
|
2018-09-09 12:05:13 -07:00
|
|
|
def signal_handler(signum, stack):
|
|
|
|
logging.critical('Got sig {}, exiting...'.format(signum))
|
|
|
|
cherrypy.engine.exit()
|
|
|
|
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
|
|
|
|
try:
|
|
|
|
cherrypy.engine.start()
|
|
|
|
cherrypy.engine.block()
|
|
|
|
finally:
|
|
|
|
logging.info("API has shut down")
|
|
|
|
cherrypy.engine.exit()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|