photolib/photoapp/api.py

287 lines
10 KiB
Python
Raw Normal View History

2019-06-17 22:43:57 -07:00
import os
import cherrypy
2019-06-25 21:34:32 -07:00
import json
2019-07-04 16:57:45 -07:00
from datetime import datetime
2019-07-20 16:05:17 -07:00
from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User, known_extensions, known_mimes, \
2019-07-09 21:10:44 -07:00
genuuid, generate_storage_path
2019-07-20 16:05:17 -07:00
from photoapp.utils import copysha, get_extension, slugify
2019-07-04 13:10:52 -07:00
from photoapp.image import special_magic_fobj
from photoapp.storage import StorageAdapter
2019-06-17 22:43:57 -07:00
from photoapp.dbutils import db
2019-07-02 23:03:28 -07:00
from contextlib import closing
2019-07-13 15:47:59 -07:00
from decimal import Decimal
2019-07-02 23:03:28 -07:00
import traceback
2019-06-17 22:43:57 -07:00
2019-06-29 12:43:13 -07:00
class LibraryManager(object):
2019-07-01 13:53:50 -07:00
def __init__(self, storage):
assert isinstance(storage, StorageAdapter)
self.storage = storage
2019-06-29 12:43:13 -07:00
2023-01-12 23:36:31 -08:00
@cherrypy.expose
2019-06-17 22:43:57 -07:00
class PhotosApi(object):
2019-07-01 13:53:50 -07:00
def __init__(self, library):
self.library = library
self.v1 = PhotosApiV1(self.library)
2019-06-17 22:43:57 -07:00
2023-01-12 23:36:31 -08:00
@cherrypy.expose
2019-06-17 22:43:57 -07:00
class PhotosApiV1(object):
2019-07-01 13:53:50 -07:00
def __init__(self, library):
2023-01-12 23:36:31 -08:00
self.library = library # TODO: move library to a cherrypy tool
self.upload = PhotosApiV1Upload(library)
self.byhash = PhotosApiV1ByHash()
self.set = PhotosApiV1Set()
self.download = PhotosApiV1Download()
self.user = PhotosApiV1User()
self.stats = PhotosApiV1Stats()
self.photos = PhotosApiV1Photos()
self.tags = PhotosApiV1PhotoTags()
def GET(self):
2019-07-04 17:46:26 -07:00
cherrypy.response.headers["Content-type"] = "text/plain"
return "Hello! This is the Photolib V1 API.\n"
2019-06-17 22:43:57 -07:00
2023-01-12 23:36:31 -08:00
@cherrypy.expose
@cherrypy.tools.json_out()
class PhotosApiV1Upload(object):
def __init__(self, library):
self.library = library
def POST(self, files, meta):
2019-06-25 21:34:32 -07:00
"""
upload accepts one photoset (multiple images)
"""
2019-11-14 21:28:32 -08:00
# TODO stage files in tmp storage since this can DEFINITELY lead to data loss
# TODO refactor function
2019-07-04 14:24:54 -07:00
stored_files = []
def abort_upload(reason):
for file in stored_files:
self.library.storage.delete(photo_path)
db.rollback()
cherrypy.response.status = 400
return {"error": reason}
2019-06-25 21:34:32 -07:00
meta = json.loads(meta)
if type(files) != list:
files = [files]
2019-07-02 12:34:41 -07:00
if set([file.filename for file in files]) != set(meta["files"].keys()):
raise cherrypy.HTTPError(400, f"file metadata missing")
2019-07-04 14:24:54 -07:00
dupes = db.query(Photo).filter(Photo.hash.in_([f["hash"] for f in meta["files"].values()])).first()
if dupes:
return abort_upload(f"file already in database: {dupes.path}")
2019-07-02 12:34:41 -07:00
# use the photo's date to build a base path
# each file's sha and file extension will be appended to this
photo_date = datetime.fromisoformat(meta["date"])
2019-07-02 23:03:28 -07:00
photo_objs = []
2019-06-25 21:34:32 -07:00
for file in files:
2019-07-02 12:34:41 -07:00
# build path using the sha and extension. note that we trust the sha the client provided now & verify later
2019-07-02 23:03:28 -07:00
# something like 2019/06/25/2019-06-25_19.28.05_cea1a138.png
2019-07-02 12:34:41 -07:00
photo_meta = meta["files"][file.filename]
ext = get_extension(file.filename)
assert ext in known_extensions
2019-07-09 21:10:44 -07:00
photo_path = generate_storage_path(photo_date, photo_meta['hash'][0:8], ext)
2019-07-04 13:10:52 -07:00
if self.library.storage.exists(photo_path):
2019-11-14 21:28:32 -08:00
pass
# we trust that the database has enforced uniqueness of this image
# return abort_upload(f"file already in library: {photo_path}")
2019-07-02 12:34:41 -07:00
# write file to the path (and copy sha while in flight)
2019-07-02 23:03:28 -07:00
with closing(self.library.storage.open(photo_path, 'wb')) as f:
shasum = copysha(file.file, f)
stored_files.append(photo_path)
2019-07-02 12:34:41 -07:00
2019-07-04 13:10:52 -07:00
# misc input validation
# also if sha doesn't match uploaded metadata, abort
2019-10-14 19:25:53 -07:00
# TODO don't use asserts
2019-07-02 23:03:28 -07:00
try:
assert shasum == photo_meta["hash"], "uploaded file didn't match provided sha"
2019-07-04 13:10:52 -07:00
with closing(self.library.storage.open(photo_path, 'rb')) as f:
mime = special_magic_fobj(f, file.filename)
assert mime == photo_meta.get("format") and mime in known_mimes, "unknown or invalid mime"
assert self.library.storage.getsize(photo_path) == photo_meta.get("size"), \
"invalid size, file truncated?"
except AssertionError as ae:
return abort_upload(str(ae))
2019-07-02 12:34:41 -07:00
# create photo object for this entry
2019-07-04 14:24:54 -07:00
p = Photo(uuid=genuuid(),
hash=shasum,
2019-07-02 23:03:28 -07:00
path=photo_path,
2019-07-04 13:10:52 -07:00
format=photo_meta.get("format"),
size=photo_meta.get("size"),
width=photo_meta.get("width"), # not verified
height=photo_meta.get("height"), # not verified
2019-07-04 14:24:54 -07:00
orientation=photo_meta.get("orientation"), # not verified
fname=photo_meta.get("fname"))
2019-07-02 23:03:28 -07:00
photo_objs.append(p)
2019-07-04 14:24:54 -07:00
for pob in photo_objs:
db.add(pob)
if meta["uuid"] is not None:
ps = db.query(PhotoSet).filter(PhotoSet.uuid == meta["uuid"]).first()
if not ps:
return abort_upload("parent uuid not found")
ps.files.extend(photo_objs)
2019-07-13 15:47:59 -07:00
if not ps.lat and meta["lat"] and meta["lon"]:
ps.lat = Decimal(meta["lat"])
ps.lon = Decimal(meta["lon"])
2019-07-04 14:24:54 -07:00
else:
ps = PhotoSet(uuid=genuuid(),
date=photo_date,
date_real=photo_date, # TODO support time offsets
2019-07-13 15:47:59 -07:00
lat=Decimal(meta["lat"]) if meta["lat"] else None,
lon=Decimal(meta["lon"]) if meta["lon"] else None,
2019-07-04 14:24:54 -07:00
files=photo_objs) # TODO support title field et
2019-10-14 19:25:53 -07:00
2019-07-04 14:24:54 -07:00
db.add(ps)
2019-10-14 19:25:53 -07:00
for tag_name in meta["tags"]:
tag = db.query(Tag).filter(Tag.name == tag_name).first()
assert tag, "unknown tag name"
db.add(TagItem(tag_id=tag.id, set=ps))
2019-07-04 14:24:54 -07:00
ps_json = ps.to_json() # we do this now to avoid a sqlalchemy bug where the object disappears after the commit
2019-07-02 23:03:28 -07:00
try:
db.commit()
2019-07-04 16:59:58 -07:00
except Exception as e:
traceback.print_exc()
return abort_upload(str(e))
2019-07-02 23:03:28 -07:00
2019-07-04 14:24:54 -07:00
return ps_json
2019-06-17 22:43:57 -07:00
2023-01-12 23:36:31 -08:00
@cherrypy.expose
@cherrypy.tools.json_out()
class PhotosApiV1ByHash(object):
def GET(self, sha):
2019-06-22 16:45:32 -07:00
f = db.query(Photo).filter(Photo.hash == sha).first()
2019-06-19 22:34:52 -07:00
if not f:
raise cherrypy.HTTPError(404)
2019-06-18 18:38:01 -07:00
return f.to_json()
2023-01-12 23:36:31 -08:00
@cherrypy.expose
@cherrypy.tools.json_out()
class PhotosApiV1Set(object):
def GET(self, uuid):
2019-06-22 16:45:32 -07:00
s = db.query(PhotoSet).filter(PhotoSet.uuid == uuid).first()
2019-06-19 22:34:52 -07:00
if not s:
raise cherrypy.HTTPError(404)
2019-06-18 18:38:01 -07:00
return s.to_json()
2023-01-12 23:36:31 -08:00
@cherrypy.expose
class PhotosApiV1Download(object):
def GET(self, uuid):
2021-07-07 19:50:15 -07:00
#TODO fix me
2019-06-22 16:45:32 -07:00
f = db.query(Photo).filter(Photo.uuid == uuid).first()
2019-06-19 22:34:52 -07:00
if not f:
raise cherrypy.HTTPError(404)
2019-06-18 18:38:01 -07:00
return cherrypy.lib.static.serve_file(os.path.abspath(os.path.join("./library", f.path)),
f.format)#TODO no hardcode path
2019-06-22 16:36:06 -07:00
2023-01-12 23:36:31 -08:00
@cherrypy.expose
@cherrypy.tools.json_out()
class PhotosApiV1User(object):
def GET(self): # list all users
return [u.to_json() for u in db.query(User).all()]
def DELETE(self, username): # delete user
u = db.query(User).filter(User.name == username).first()
if not u:
raise cherrypy.HTTPError(404)
db.delete(u)
return "ok"
def POST(self, username, password_hash): # create/update user
u = db.query(User).filter(User.name == username).first()
if u:
u.password = password_hash
else:
db.add(User(name=username, password=password_hash))
2019-06-22 16:36:06 -07:00
return "ok"
2019-07-04 16:57:45 -07:00
2023-01-12 23:36:31 -08:00
@cherrypy.expose
@cherrypy.tools.json_out()
class PhotosApiV1Stats(object):
def GET(self):
2019-07-04 16:57:45 -07:00
return {"photos": db.query(PhotoSet).count(),
"files": db.query(Photo).count(),
"tags": db.query(Tag).count(),
"users": db.query(User).count(),
"public_photos": db.query(PhotoSet).filter(PhotoSet.status == PhotoStatus.public).count()}
2023-01-12 23:36:31 -08:00
@cherrypy.expose
@cherrypy.tools.json_out()
@cherrypy.popargs("uuid")
class PhotosApiV1Photos(object):
def GET(self, uuid=None, page=0, pagesize=50):
2019-07-04 16:57:45 -07:00
if uuid:
p = db.query(PhotoSet).filter(PhotoSet.uuid == uuid).first()
if not p:
cherrypy.response.status = 404
return {"error": "not found"}
return p.to_json()
else:
page, pagesize = int(page), int(pagesize)
return [p.to_json() for p in
db.query(PhotoSet).order_by(PhotoSet.id).offset(pagesize * page).limit(pagesize).all()]
2019-07-20 16:05:17 -07:00
2023-01-12 23:36:31 -08:00
@cherrypy.expose
@cherrypy.tools.json_out()
@cherrypy.popargs("uuid")
class PhotosApiV1PhotoTags(object):
def GET(self, uuid=None, name=None, page=0, pagesize=50): # getting tag/s
if uuid or name:
2019-10-14 19:25:53 -07:00
q = db.query(Tag)
if uuid:
q = q.filter(Tag.uuid == uuid)
if name:
q = q.filter(Tag.name == name)
t = q.first()
2019-07-20 16:05:17 -07:00
if not t:
cherrypy.response.status = 404
return {"error": "not found"}
2019-10-14 19:25:53 -07:00
return [t.to_json()]
2019-07-20 16:05:17 -07:00
else: # getting all tags
page, pagesize = int(page), int(pagesize)
return [t.to_json() for t in
db.query(Tag).order_by(Tag.id).offset(pagesize * page).limit(pagesize).all()]
2023-01-12 23:36:31 -08:00
def DELETE(self, uuid): # deleting tag
tag = db.query(Tag).filter(Tag.uuid == uuid).first()
db.query(TagItem).fitler(TagItem.tag_id == tag.id).delete()
db.delete(tag)
db.commit()
return {}
def POST(self): # creating tag
tagdata = json.loads(cherrypy.request.body.read())
tagname = tagdata.get("name")
db.add(Tag(name=tagname,
title=tagdata.get("title", None) or tagname.capitalize(),
description=tagdata.get("description", None),
slug=slugify(tagname)))
db.commit()
return {}