From bc64acce2a997ed2b42cc223574eb1655843c4e5 Mon Sep 17 00:00:00 2001 From: dave Date: Thu, 4 Jul 2019 16:57:45 -0700 Subject: [PATCH] listing apis --- photoapp/api.py | 75 +++++++++++++++++----------------------------- photoapp/cli.py | 47 +++++++++++++++++++++++++++-- photoapp/daemon.py | 1 - photoapp/types.py | 8 +++-- photoapp/users.py | 2 +- 5 files changed, 77 insertions(+), 56 deletions(-) diff --git a/photoapp/api.py b/photoapp/api.py index 3fd1270..afcdfd6 100644 --- a/photoapp/api.py +++ b/photoapp/api.py @@ -1,21 +1,11 @@ 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, known_extensions, known_mimes, genuuid -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, copysha, get_extension +from datetime import datetime +from photoapp.types import Photo, PhotoSet, Tag, PhotoStatus, User, known_extensions, known_mimes, genuuid +from photoapp.utils import copysha, get_extension from photoapp.image import special_magic_fobj from photoapp.dbutils import db -import tempfile from contextlib import closing import traceback @@ -95,41 +85,6 @@ class LibraryManager(object): 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): @@ -290,3 +245,27 @@ class PhotosApiV1(object): else: db.add(User(name=username, password=password_hash)) return "ok" + + @cherrypy.expose + @cherrypy.tools.json_out() + def stats(self): + 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()} + + @cherrypy.expose + @cherrypy.tools.json_out() + @cherrypy.popargs("uuid") + def photos(self, uuid=None, page=0, pagesize=50): + 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()] diff --git a/photoapp/cli.py b/photoapp/cli.py index 171dbdc..a531c38 100644 --- a/photoapp/cli.py +++ b/photoapp/cli.py @@ -44,9 +44,20 @@ class PhotoApiClient(object): return self.delete("user", params={"username": username}) def upload(self, files, metadata): - # print(">>>>>>", metadata) return self.post("upload", files=files, data={"meta": json.dumps(metadata)}) + def stats(self): + return self.get("stats").json() + + def list_photos(self, page=0, pagesize=25): + return self.get("photos", params={"page": page, "pagesize": pagesize}).json() + + +def maybetruncate(s, length): + if s and len(s) > length: + s = s[0:length - 3] + "..." + return s + def get_args(): parser = argparse.ArgumentParser(description="photo library cli") @@ -65,6 +76,13 @@ def get_args(): p_ingest.add_argument("-c", "--copy-of", help="existing uuid the imported images will be placed under") p_ingest.add_argument("files", nargs="+", help="files to import") + sp_action.add_parser("stats", help="show library statistics") + + p_list = sp_action.add_parser("list", help="list images in library") + p_list.add_argument("-p", "--page", default=0, help="page number") + p_list.add_argument("-z", "--page-size", default=25, help="page size") + p_list.add_argument("-l", "--show-link", action="store_true", help="print urls to each photo") + # User section p_adduser = sp_action.add_parser("user", help="user manipulation functions") p_useraction = p_adduser.add_subparsers(dest="action_user", help="action to take") @@ -83,7 +101,6 @@ def get_args(): def main(): args = get_args() - print(args) client = PhotoApiClient(args.host, (args.user, args.password, )) @@ -107,7 +124,7 @@ def main(): raise NotImplementedError("must pass --sha-files for now") for sha, path in hashes.items(): - # hit http://localhost:8080/api/v1/byhash?sha=afe49172f709725a4503c9219fb4c6a9db8ad0354fc493f2f500269ac6faeaf6 + # http://localhost:8080/api/v1/byhash?sha=afe49172f709725a4503c9219fb4c6a9db8ad0354fc493f2f500269ac6faeaf6 try: client.byhash(sha) # if the file is a dupe, do nothing @@ -147,6 +164,30 @@ def main(): print(f"{num} / {len(sets)}") # TODO be nice and close the files + elif args.action == "list": + headers = ["link" if args.show_link else "uuid", + "status", + "size", + "formats", + "date", + "title"]# TODO tags + rows = [] + for set_ in client.list_photos(args.page, args.page_size): + row = [set_["uuid"] if not args.show_link else args.host.rstrip("/") + "/photo/" + set_["uuid"], + PhotoStatus[set_["status"]].name, + sum([p["size"] for p in set_["files"].values()]), + " ".join(set([p["format"] for p in set_["files"].values()])), + set_["date"], + maybetruncate(set_["title"], 24), + ] + rows.append(row) + print(tabulate(rows, headers=headers)) + + elif args.action == "stats": + print(tabulate(sorted([[k, v] for k, v in client.stats().items()], + key=lambda row: row[0], reverse=True), + headers=["item", "count"])) + elif args.action == "user": if args.action_user == "create": print(client.create_user(args.username, args.password).json()) diff --git a/photoapp/daemon.py b/photoapp/daemon.py index cbf1e27..103c4a8 100644 --- a/photoapp/daemon.py +++ b/photoapp/daemon.py @@ -263,7 +263,6 @@ class ThumbnailView(object): best = photo break thumb_from = best or first - print(repr(thumb_from)) if not thumb_from: raise cherrypy.HTTPError(404) # TODO some lock around calls to this based on uuid diff --git a/photoapp/types.py b/photoapp/types.py index dcf332f..5b5212d 100644 --- a/photoapp/types.py +++ b/photoapp/types.py @@ -66,13 +66,15 @@ class PhotoSet(Base): status = Column(Enum(PhotoStatus), default=PhotoStatus.private) - def to_json(self): + def to_json(self, files=True): s = {attr: getattr(self, attr) for attr in {"uuid", "title", "description"}} s["lat"] = str(self.lat) if self.lat else None s["lon"] = str(self.lon) if self.lon else None s["date"] = self.date.isoformat() - s["files"] = {i.uuid: i.to_json() for i in self.files} - s["tags"] = [t.name for t in self.tags] + if files: + s["files"] = {i.uuid: i.to_json() for i in self.files} + s["tags"] = [t.tag.name for t in self.tags] + s["status"] = self.status.name if self.status else PhotoStatus.private.name # duplicate default definition return s diff --git a/photoapp/users.py b/photoapp/users.py index cb3c654..a9c270c 100644 --- a/photoapp/users.py +++ b/photoapp/users.py @@ -33,7 +33,7 @@ def main(): p_create.add_argument("-u", "--username", help="username", required=True) p_create.add_argument("-p", "--password", help="password", required=True) - p_list = p_mode.add_parser('list', help='list users') + p_mode.add_parser('list', help='list users') p_delete = p_mode.add_parser('delete', help='delete users') p_delete.add_argument("-u", "--username", help="username", required=True)