22import json
33import logging
44import os
5+ import pathlib
6+ from datetime import datetime
7+ from typing import Optional
58
69from aiohttp import web
7-
810from core .addons .api .dto .details import Details
911from core .addons .api .dto .location import Location
10- from core .addons .api .dto .photo import Photo , PhotoEncoder
11- from core .addons .api .dto .photo_response import PhotoResponse
12- from core .addons .api .dto .photo_url import PhotoUrl
12+ from core .addons .api .dto .photo import (PhotoDetailsResponse , PhotoEncoder ,
13+ PhotoResponse )
14+ from core .addons .api .dto .photo_response import PhotosResponse
15+ from core .base import Session
1316from core .core import ApplicationCore
17+ from core .persistency .dto .photo import Photo
1418from core .webserver .request import KEY_USER_ID , RequestView
1519from core .webserver .status import HTTP_CREATED , HTTP_OK
1620
@@ -28,6 +32,7 @@ async def async_setup(core: ApplicationCore, config: dict) -> bool:
2832
2933 core .http .register_request (APIStatusView ())
3034 core .http .register_request (PhotosView ())
35+ core .http .register_request (PhotoDetailsView ())
3136 core .http .register_request (PhotoView ())
3237 core .http .register_request (AlbumView ())
3338
@@ -53,93 +58,133 @@ async def head(self, core: ApplicationCore, request: web.Request):
5358class PhotosView (RequestView ):
5459 """View to handle photos requests."""
5560
56- url = "/api/photo/ "
57- name = "api :photo:upload "
61+ url = "/v1/photos "
62+ name = "v1 :photo"
5863
5964 async def get (self , core : ApplicationCore , request : web .Request ) -> web .Response :
6065 """Get a list of all photo resources."""
66+ _LOGGER .debug (f"GET /v1/photos" )
67+ await core .authentication .check_permission (request , "library:read" )
68+
6169 user_id = await core .http .get_user_id (request )
6270
6371 if user_id is None :
6472 raise web .HTTPForbidden ()
6573
66- _LOGGER .debug (f"read photos for user_id { user_id } " )
67- user_photos = await core .storage .read_photos (user_id )
68- _LOGGER .debug (f"iterate through { len (user_photos )} photos." )
74+ limit = 50
75+ if "limit" in request .query :
76+ limit = int (request .query ["limit" ])
77+
78+ offset = 0
79+ if "offset" in request .query :
80+ offset = int (request .query ["offset" ])
81+
82+ _LOGGER .debug (f"read { limit } photos for user_id { user_id } beginning with { offset } " )
83+ user_photos = await core .storage .read_photos (user_id , offset , limit )
6984
7085 results = []
7186
72- # iterate through photos
7387 for photo in user_photos :
74- _LOGGER .debug (f"get additional data for { photo .filepath } " )
75-
76- # photo location
77- location = None
78- latitude = await core .storage .read ("latitude" )
79- longitude = await core .storage .read ("longitude" )
80- if latitude is not None and longitude is not None :
81- altitude = await core .storage .read ("altitude" )
82- if altitude is not None :
83- location = Location (
84- latitude = latitude , longitude = longitude , altitude = altitude
85- )
86- else :
87- location = Location (
88- latitude = latitude , longitude = longitude , altitude = "0.0"
89- )
90-
91- # photo tags
92- tags = await core .storage .read ("tags" )
93- tags = ["landscape" , "sky" , "night" ]
94-
95- # add photo to results
9688 results .append (
97- Photo (
98- name = "DSC_2340-HDR.jpg" ,
99- description = "" ,
100- author = "" ,
101- created_at = "2012-02-09T21:11:53-05:00" ,
102- details = Details (
103- camera = "Canon EOS-1D Mark IV" ,
104- lens = "E 18-200mm F3.5-6.3 OSS" ,
105- focal_length = "700" ,
106- iso = "400" ,
107- shutter_speed = "1/2000" ,
108- aperture = "6.3" ,
109- ),
110- tags = tags ,
111- location = location ,
112- image_urls = [
113- PhotoUrl (size = "1080" , url = "/data/cache/DSC_2340-HDR_1080.jpg" ),
114- PhotoUrl (size = "1600" , url = "/data/cache/DSC_2340-HDR_1600.jpg" ),
115- PhotoUrl (size = "2048" , url = "/data/cache/DSC_2340-HDR_2048.jpg" ),
116- PhotoUrl (size = "full" , url = "/data/cache/DSC_2340-HDR.jpg" ),
117- ],
89+ PhotoResponse (
90+ id = photo .uuid ,
91+ name = photo .filename ,
92+ image_url = f"{ core .config .external_url } /v1/file/{ photo .uuid } "
11893 )
11994 )
12095
121- # key = "latitude"
122- # _LOGGER.error(f"key/value: {key}/{value}")
123-
124- # data = request.query
96+ response = PhotosResponse (offset = offset , limit = limit , size = len (results ), results = results )
97+ return web .Response (text = json .dumps (response , cls = PhotoEncoder ), content_type = "application/json" )
12598
126- offset = 0
127- # if data["size"]:
128- # offset = data["size"] # integer 0..N
12999
130- limit = 50
131- # if data["size"]: # integer Number of records per page.
132- # limit = data["size"]
100+ class PhotoDetailsView (RequestView ):
101+ """View to handle single photo requests."""
133102
134- _LOGGER .info (f"loading data for user { user_id } " )
103+ url = "/v1/photo/{entity_id}"
104+ name = "v1:photo"
135105
136- response = PhotoResponse (
137- offset = offset , limit = limit , size = len (results ), results = results
106+ async def get (self , core : ApplicationCore , request : web .Request , entity_id : str ) -> web .Response :
107+ """Return an entity."""
108+ _LOGGER .debug (f"GET /v1/photo/{ entity_id } " )
109+
110+ # TODO: add user_id to check if user has access to image
111+ photo = await core .storage .read_photo (entity_id )
112+
113+ if photo is None :
114+ raise web .HTTPNotFound
115+
116+ # photo owner
117+ # TODO: get first-/lastname of owner
118+ # owner = await core.authentication
119+
120+ _LOGGER .debug (f"photo { photo .uuid } " )
121+ file = os .path .join (photo .directory , photo .filename )
122+ _LOGGER .debug (f"get additional data for { file } / { os .path .exists (file )} " )
123+
124+ # photo creation time
125+ fname = pathlib .Path (file )
126+ mtime = datetime .fromtimestamp (fname .stat ().st_mtime )
127+ ctime = datetime .fromtimestamp (fname .stat ().st_ctime )
128+
129+ # photo location
130+ location = None
131+ latitude = await core .storage .read ("latitude" )
132+ longitude = await core .storage .read ("longitude" )
133+ if latitude is not None and longitude is not None :
134+ altitude = await core .storage .read ("altitude" )
135+ if altitude is not None :
136+ location = Location (latitude = latitude , longitude = longitude , altitude = altitude )
137+ else :
138+ location = Location (latitude = latitude , longitude = longitude , altitude = "0.0" )
139+
140+ # photo tags
141+ tags = await core .storage .read ("tags" )
142+
143+ result = PhotoDetailsResponse (
144+ id = photo .uuid ,
145+ name = photo .filename ,
146+ author = photo .owner ,
147+ created_at = ctime .isoformat (),
148+ details = Details (
149+ camera = "Nikon Z7" ,
150+ lens = "Nikkor 200mm F1.8" ,
151+ focal_length = "200" ,
152+ iso = "400" ,
153+ shutter_speed = "1/2000" ,
154+ aperture = "4.0" ,
155+ ),
156+ tags = tags ,
157+ location = location ,
158+ image_url = f"{ core .config .external_url } /v1/file/{ entity_id } "
138159 )
160+ return web .Response (text = json .dumps (result , cls = PhotoEncoder ), content_type = "application/json" )
139161
140- return web .Response (
141- text = json .dumps (response , cls = PhotoEncoder ), content_type = "application/json"
142- )
162+
163+ class PhotoView (RequestView ):
164+ """View to handle photo file requests."""
165+
166+ # TODO: enable auth
167+ requires_auth = False
168+ url = "/v1/file/{entity_id}"
169+ name = "v1:file"
170+
171+ async def get (self , core : ApplicationCore , request : web .Request , entity_id : str ) -> web .Response :
172+ """Return an entity."""
173+ _LOGGER .debug (f"GET /v1/file/{ entity_id } " )
174+
175+ # TODO: parse params max-with / max-height =wmax-width-hmax-height (=w2048-h1024)
176+ # -wmax-width (preserving the aspect ratio)
177+ # -hmax-height (preserving the aspect ratio)
178+ # -c crop images to max-width / max-height
179+ # -d remove exif data
180+
181+ result = Session .query (Photo ).filter (Photo .uuid == entity_id ).first ()
182+
183+ file = os .path .join (result .directory , result .filename )
184+ if os .path .exists (os .path .join (file )):
185+ return web .FileResponse (path = file , status = 200 )
186+ else :
187+ raise web .HTTPNotFound ()
143188
144189 async def post (self , core : ApplicationCore , request : web .Request ) -> web .Response :
145190 """Upload new photo resource."""
@@ -179,30 +224,16 @@ async def post(self, core: ApplicationCore, request: web.Request) -> web.Respons
179224
180225 status_code = HTTP_CREATED if new_entity_created else HTTP_OK
181226
182- resp = self .json_message (
183- f"File successfully added with ID: { new_entity_id } " , status_code
184- )
227+ resp = self .json_message (f"File successfully added with ID: { new_entity_id } " , status_code )
185228 resp .headers .add ("Location" , f"/api/photo/{ new_entity_id } " )
186229
187230 return resp
188231
189-
190- class PhotoView (RequestView ):
191- """View to handle single photo requests."""
192-
193- url = "/api/photo/{entity_id}"
194- name = "api:photo"
195-
196- async def get (self , request : web .Request , entity_id ) -> web .Response :
197- """Return an entity."""
198- return self .json_message (f"return GET { entity_id } " )
199-
200- async def post (self , request : web .Request , entity_id ):
201- """Create an entity."""
202- return self .json_message (f"return POST { entity_id } " )
203-
204- async def delete (self , request : web .Request , entity_id ):
232+ async def delete (self , core : ApplicationCore , request : web .Request , entity_id : str ):
205233 """Delete an entity."""
234+ _LOGGER .debug (f"DELETE /v1/file/{ entity_id } " )
235+
236+ # TODO: delete entity
206237 return self .json_message (f"return DELETE { entity_id } " )
207238
208239
0 commit comments