photolib/photoapp/api.py

227 lines
7.5 KiB
Python

import os
import cherrypy
import logging
import json
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 sqlalchemy.exc import IntegrityError
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 StorageAdapter(object):
"""
Abstract interface for working with photo file storage. All paths are relative to the storage adapter's root parameter.
"""
def file_exists(self, path):
# TODO return true/false if the file path exists
raise NotImplementedError()
def open(self, path, mode):
# TODO return a handle to the path
# TODO this should work as a context manager
raise NotImplementedError()
def delete(self, path):
# TODO erase the path
raise NotImplementedError()
def dedupe_name(self, path):
# TODO modify and return the passed path such that writing to it does not overwrite an existing file
raise NotImplementedError()
class FilesystemAdapter(StorageAdapter):
def __init__(self, root):
super().__init__()
self.root = root # root path
def file_exists(self, path):
# TODO return true/false if the file path exists
raise NotImplementedError()
def open(self, path, mode):
# TODO return a handle to the path. this should work as a context manager
raise NotImplementedError()
def delete(self, path):
# TODO erase the path
raise NotImplementedError()
def dedupe_name(self, path):
# TODO modify and return the passed path such that writing to it does not overwrite an existing file
raise NotImplementedError()
class S3Adapter(StorageAdapter):
def file_exists(self, path):
# TODO return true/false if the file path exists
raise NotImplementedError()
def open(self, path, mode):
# TODO return a handle to the path. this should work as a context manager
raise NotImplementedError()
def delete(self, path):
# TODO erase the path
raise NotImplementedError()
def dedupe_name(self, path):
# TODO modify and return the passed path such that writing to it does not overwrite an existing file
raise NotImplementedError()
class GfapiAdapter(StorageAdapter):
pass # TODO gluster storage backend
#This is largely duplicated from library.py, but written with intent for later refactoring to support abstract storage.
class LibraryManager(object):
def __init__(self, storage):
assert isinstance(storage, StorageAdapter)
self.storage = storage
def add_photoset(self, photoset):
"""
Commit a populated photoset object to the library. The paths in the photoset's file list entries will be updated
as the file is moved to the library path.
"""
# Create target directory
path = os.path.join(self.path, self.get_datedir_path(photoset.date))
os.makedirs(path, exist_ok=True)
moves = [] # Track files moved. If the sql transaction files, we'll undo these
for file in photoset.files:
dest = os.path.join(path, os.path.basename(file.path))
# Check if the name is already in use, rename new file if needed
dupe_rename = 1
while os.path.exists(dest):
fname = os.path.basename(file.path).split(".")
fname[-2] += "_{}".format(dupe_rename)
dest = os.path.join(path, '.'.join(fname))
dupe_rename += 1
os.rename(file.path, dest)
moves.append((file.path, dest))
file.path = dest.lstrip(self.path)
s = self.session()
s.add(photoset)
try:
s.commit()
except IntegrityError:
# Commit failed, undo the moves
for move in moves:
os.rename(move[1], move[0])
raise
class PhotosApi(object):
def __init__(self, library):
self.library = library
self.v1 = PhotosApiV1(self.library)
class PhotosApiV1(object):
def __init__(self, library):
self.library = library
@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):
"""
upload accepts one photoset (multiple images)
metadata format
"""
meta = json.loads(meta)
if type(files) != list:
files = [files]
for file in files:
print("File name:", file.filename)
# import hashlib
# sha = hashlib.sha256()
# total = 0
# while True:
# b = file.file.read(1024)
# if not b:
# break
# sha.update(b)
# total += len(b)
# print("Read length:", total)
# print("Read sha256:", sha.hexdigest())
if str(file.filename) not in meta["files"].keys():
raise cherrypy.HTTPError(400, f"no mdatadata provided for filename '{file.filename}'")
print("we have metadata for this file:", meta["files"][file.filename])
# create database objects based on the request
# self.lib.add_photoset(set_, photos)
# build file path (yyyy/mm/dd/yyyy-mm_hh.MM.ss_x.jpg) (incrmenting X if the key already exists etc)
# copy to storage
# check if sha256 exists already
# delete if dupe, raise error
# (see file rewind code in ingest.py)
# create records
# commit
# respond with list of uuids of the sets
print("____")
@cherrypy.expose
@cherrypy.tools.json_out()
def byhash(self, sha):
f = db.query(Photo).filter(Photo.hash == sha).first()
if not f:
raise cherrypy.HTTPError(404)
return f.to_json()
@cherrypy.expose
@cherrypy.tools.json_out()
def set(self, uuid):
s = db.query(PhotoSet).filter(PhotoSet.uuid == uuid).first()
if not s:
raise cherrypy.HTTPError(404)
return s.to_json()
@cherrypy.expose
def download(self, uuid):
f = db.query(Photo).filter(Photo.uuid == uuid).first()
if not f:
raise cherrypy.HTTPError(404)
return cherrypy.lib.static.serve_file(os.path.abspath(os.path.join("./library", f.path)),
f.format)#TODO no hardcode path
@cherrypy.expose
@cherrypy.tools.json_out()
def user(self, username=None, password_hash=None):
if username is None: # list all users
return [u.to_json() for u in db.query(User).all()]
elif username and cherrypy.request.method == "DELETE": # delete user
u = db.query(User).filter(User.name == username).first()
if not u:
raise cherrypy.HTTPError(404)
db.delete(u)
elif username and 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))
return "ok"