mirror of
https://github.com/enpaul/kodak.git
synced 2024-11-23 15:07:13 +00:00
Update resources and flask plumbing to remove previuos scope creep
This commit is contained in:
parent
b61ef4624b
commit
99d2ca4816
@ -1,11 +1,8 @@
|
|||||||
import flask
|
import flask
|
||||||
|
|
||||||
from fresnel_lens import __about__
|
|
||||||
from fresnel_lens import configuration
|
from fresnel_lens import configuration
|
||||||
from fresnel_lens import constants
|
|
||||||
from fresnel_lens import database
|
from fresnel_lens import database
|
||||||
from fresnel_lens import exceptions
|
from fresnel_lens import exceptions
|
||||||
from fresnel_lens.resources import ResponseHeaders
|
|
||||||
|
|
||||||
|
|
||||||
def make_the_tea() -> None:
|
def make_the_tea() -> None:
|
||||||
@ -24,31 +21,12 @@ def initialize_database() -> None:
|
|||||||
database.initialize(flask.current_app.appconfig)
|
database.initialize(flask.current_app.appconfig)
|
||||||
|
|
||||||
|
|
||||||
class FresnelRequest(flask.Request):
|
class FresnelFlask(flask.Flask):
|
||||||
"""Extend the default Flask request object to add custom application state settings"""
|
|
||||||
|
|
||||||
def make_response_headers(self) -> ResponseHeaders:
|
|
||||||
"""Create the headers dictionary of the standard response headers
|
|
||||||
|
|
||||||
This function should be used when determining response headers so that the header names,
|
|
||||||
their contents, and formatting are universal.
|
|
||||||
|
|
||||||
:returns: Dictionary of headers
|
|
||||||
"""
|
|
||||||
|
|
||||||
return {
|
|
||||||
constants.HTTP_HEADER_RESPONSE_VERSION: __about__.__version__,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ImageMuckFlask(flask.Flask):
|
|
||||||
"""Extend the default Flask object to add the custom application config
|
"""Extend the default Flask object to add the custom application config
|
||||||
|
|
||||||
There's probably an easier/more kosher way to do this, but ¯\\_(ツ)_/¯
|
There's probably an easier/more kosher way to do this, but ¯\\_(ツ)_/¯
|
||||||
"""
|
"""
|
||||||
|
|
||||||
request_class = FresnelRequest
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.appconfig: configuration.FresnelConfig = configuration.load()
|
self.appconfig: configuration.FresnelConfig = configuration.load()
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
import flask_restful
|
import flask_restful
|
||||||
|
|
||||||
from fresnel_lens import resources
|
from fresnel_lens import resources
|
||||||
from fresnel_lens._server import ImageMuckFlask
|
from fresnel_lens._server import FresnelFlask
|
||||||
from fresnel_lens._server import initialize_database
|
from fresnel_lens._server import initialize_database
|
||||||
from fresnel_lens._server import make_the_tea
|
from fresnel_lens._server import make_the_tea
|
||||||
|
|
||||||
|
|
||||||
APPLICATION = ImageMuckFlask(__name__)
|
APPLICATION = FresnelFlask(__name__)
|
||||||
API = flask_restful.Api(APPLICATION, catch_all_404s=True)
|
API = flask_restful.Api(APPLICATION, catch_all_404s=True)
|
||||||
|
|
||||||
|
|
||||||
def _set_upload_limit() -> None:
|
|
||||||
APPLICATION.config["MAX_CONTENT_LENGTH"] = APPLICATION.appconfig.upload.size_limit
|
|
||||||
|
|
||||||
|
|
||||||
APPLICATION.before_request(make_the_tea)
|
APPLICATION.before_request(make_the_tea)
|
||||||
APPLICATION.before_first_request(initialize_database)
|
APPLICATION.before_first_request(initialize_database)
|
||||||
APPLICATION.before_first_request(_set_upload_limit)
|
|
||||||
|
|
||||||
for resource in resources.RESOURCES:
|
for resource in resources.RESOURCES:
|
||||||
API.add_resource(resource, *resource.routes)
|
API.add_resource(resource, *resource.routes)
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from fresnel_lens.resources._shared import FresnelResource
|
from fresnel_lens.resources._shared import FresnelResource
|
||||||
from fresnel_lens.resources._shared import ResponseBody
|
from fresnel_lens.resources.alias import ImageAlias
|
||||||
from fresnel_lens.resources._shared import ResponseHeaders
|
from fresnel_lens.resources.heartbeat import Heartbeat
|
||||||
from fresnel_lens.resources.image import Image
|
from fresnel_lens.resources.image import Image
|
||||||
from fresnel_lens.resources.image import ImageUpload
|
|
||||||
from fresnel_lens.resources.openapi import OpenAPI
|
from fresnel_lens.resources.openapi import OpenAPI
|
||||||
from fresnel_lens.resources.thumbnail import ThumbnailResize
|
|
||||||
from fresnel_lens.resources.thumbnail import ThumbnailScale
|
|
||||||
|
|
||||||
|
|
||||||
RESOURCES: Tuple[FresnelResource, ...] = (
|
RESOURCES: Tuple[FresnelResource, ...] = (
|
||||||
ImageUpload,
|
Heartbeat,
|
||||||
Image,
|
Image,
|
||||||
|
ImageAlias,
|
||||||
OpenAPI,
|
OpenAPI,
|
||||||
ThumbnailScale,
|
|
||||||
ThumbnailResize,
|
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,8 @@ from typing import Union
|
|||||||
import flask
|
import flask
|
||||||
import flask_restful
|
import flask_restful
|
||||||
|
|
||||||
|
from fresnel_lens import __about__
|
||||||
|
|
||||||
|
|
||||||
ResponseBody = Optional[Union[Dict[str, Any], List[Dict[str, Any]], List[str]]]
|
ResponseBody = Optional[Union[Dict[str, Any], List[Dict[str, Any]], List[str]]]
|
||||||
|
|
||||||
@ -99,7 +101,7 @@ class FresnelResource(flask_restful.Resource):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
headers = {**headers, **flask.request.make_response_headers()}
|
headers.update({"Server": f"{__about__.__title__}-{__about__.__version__}"})
|
||||||
|
|
||||||
# 204 code specifies that it must never include a response body. Most clients will ignore
|
# 204 code specifies that it must never include a response body. Most clients will ignore
|
||||||
# any response body when a 204 is given, but that's no reason to abandon best practices here
|
# any response body when a 204 is given, but that's no reason to abandon best practices here
|
||||||
|
10
fresnel_lens/resources/alias.py
Normal file
10
fresnel_lens/resources/alias.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from fresnel_lens.resources._shared import FresnelResource
|
||||||
|
from fresnel_lens.resources._shared import ResponseTuple
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAlias(FresnelResource):
|
||||||
|
|
||||||
|
routes = ("/image/<string:image_name>/<string:alias>",)
|
||||||
|
|
||||||
|
def get(self, image_name: str, alias: str) -> ResponseTuple:
|
||||||
|
raise NotImplementedError
|
18
fresnel_lens/resources/heartbeat.py
Normal file
18
fresnel_lens/resources/heartbeat.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from fresnel_lens import configuration
|
||||||
|
from fresnel_lens import database
|
||||||
|
from fresnel_lens.resources._shared import FresnelResource
|
||||||
|
from fresnel_lens.resources._shared import ResponseTuple
|
||||||
|
|
||||||
|
|
||||||
|
class Heartbeat(FresnelResource):
|
||||||
|
|
||||||
|
routes = ("/heartbeat",)
|
||||||
|
|
||||||
|
def get(self) -> ResponseTuple:
|
||||||
|
configuration.load()
|
||||||
|
database.ImageRecord.select().count()
|
||||||
|
|
||||||
|
return self.make_response(None)
|
||||||
|
|
||||||
|
def head(self) -> ResponseTuple:
|
||||||
|
return self._head(self.get())
|
@ -1,113 +1,10 @@
|
|||||||
import hashlib
|
|
||||||
import shutil
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import flask
|
|
||||||
|
|
||||||
from fresnel_lens import constants
|
|
||||||
from fresnel_lens import database
|
|
||||||
from fresnel_lens import exceptions
|
|
||||||
from fresnel_lens.resources._shared import FresnelResource
|
from fresnel_lens.resources._shared import FresnelResource
|
||||||
|
from fresnel_lens.resources._shared import ResponseTuple
|
||||||
|
|
||||||
class ImageUpload(FresnelResource):
|
|
||||||
|
|
||||||
routes = ("/image/",)
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
if "image" not in flask.request.files:
|
|
||||||
raise
|
|
||||||
|
|
||||||
uploaded = flask.request.files["image"]
|
|
||||||
|
|
||||||
if not uploaded.filename:
|
|
||||||
raise
|
|
||||||
|
|
||||||
format = uploaded.filename.rpartition(".")[-1].lower()
|
|
||||||
|
|
||||||
if format not in flask.current_app.appconfig.upload.formats:
|
|
||||||
raise
|
|
||||||
|
|
||||||
image = database.ImageRecord(format=format, width=0, height=0, owner="foobar")
|
|
||||||
|
|
||||||
imagedir = flask.current_app.appconfig.storage_path / str(image.uuid)
|
|
||||||
|
|
||||||
imagedir.mkdir()
|
|
||||||
|
|
||||||
uploaded.save(imagedir / f"base.{format}")
|
|
||||||
|
|
||||||
with (imagedir / f"base.{format}").open() as infile:
|
|
||||||
image.sha256 = hashlib.sha256(infile.read()).hexdigest()
|
|
||||||
|
|
||||||
with database.interface.atomic():
|
|
||||||
image.save()
|
|
||||||
|
|
||||||
return None, 201
|
|
||||||
|
|
||||||
|
|
||||||
class Image(FresnelResource):
|
class Image(FresnelResource):
|
||||||
|
|
||||||
routes = ("/image/<string:image_id>.jpeg",)
|
routes = ("/image/<string:image_name>",)
|
||||||
|
|
||||||
def get(self, image_id: str):
|
def get(self, image_name: str) -> ResponseTuple:
|
||||||
|
raise NotImplementedError
|
||||||
image = database.ImageRecord.get(
|
|
||||||
database.ImageRecord.uuid == uuid.UUID(image_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
if image.deleted:
|
|
||||||
raise exceptions.ImageResourceDeletedError(
|
|
||||||
f"Image with ID '{image_id}' was deleted"
|
|
||||||
)
|
|
||||||
|
|
||||||
filepath = (
|
|
||||||
flask.current_app.appconfig.storage_path
|
|
||||||
/ str(image.uuid)
|
|
||||||
/ f"base.{image.format}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not filepath.exists():
|
|
||||||
with database.interface.atomic():
|
|
||||||
image.deleted = True
|
|
||||||
image.save()
|
|
||||||
raise exceptions.ImageFileRemovedError(
|
|
||||||
f"Image file with ID '{image_id}' removed from the server"
|
|
||||||
)
|
|
||||||
|
|
||||||
flask.send_file(
|
|
||||||
filepath,
|
|
||||||
mimetype=f"image/{'jpeg' if image.format == 'jpg' else image.format}",
|
|
||||||
# images are indexed by UUID with no ability to update, y'all should cache
|
|
||||||
# this thing 'till the sun explodes
|
|
||||||
cache_timeout=(60 * 60 * 24 * 365),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
None,
|
|
||||||
200,
|
|
||||||
{constants.HTTP_HEADER_RESPONSE_DIGEST: f"sha-256={image.sha256}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, image_id: str, format: str):
|
|
||||||
|
|
||||||
image = database.ImageRecord.get(
|
|
||||||
database.ImageRecord.uuid
|
|
||||||
== uuid.UUID(image_id) & database.ImageRecord.format
|
|
||||||
== format
|
|
||||||
)
|
|
||||||
|
|
||||||
if image.deleted:
|
|
||||||
raise exceptions.ImageResourceDeletedError(
|
|
||||||
f"Image with ID '{image_id}' was deleted"
|
|
||||||
)
|
|
||||||
|
|
||||||
filepath = flask.current_app.appconfig.storage_path / str(image.uuid)
|
|
||||||
|
|
||||||
with database.interface.atomic():
|
|
||||||
image.deleted = True
|
|
||||||
image.save()
|
|
||||||
|
|
||||||
if filepath.exists():
|
|
||||||
shutil.rmtree(filepath)
|
|
||||||
|
|
||||||
return None, 204
|
|
||||||
|
@ -3,17 +3,23 @@ from pathlib import Path
|
|||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
from fresnel_lens.resources._shared import FresnelResource
|
from fresnel_lens.resources._shared import FresnelResource
|
||||||
|
from fresnel_lens.resources._shared import ResponseTuple
|
||||||
|
|
||||||
yaml = YAML(typ="safe")
|
yaml = YAML(typ="safe")
|
||||||
|
|
||||||
|
|
||||||
class OpenAPI(FresnelResource):
|
class OpenAPI(FresnelResource):
|
||||||
|
"""Handle requests for the OpenAPI specification resource"""
|
||||||
|
|
||||||
routes = ("/openapi.json",)
|
routes = ("/openapi.json",)
|
||||||
|
|
||||||
def get(self):
|
def get(self) -> ResponseTuple:
|
||||||
|
"""Retrieve the OpenAPI specification document"""
|
||||||
with (Path(__file__).parent, "openapi.yaml").open() as infile:
|
with (Path(__file__).parent / "openapi.yaml").open() as infile:
|
||||||
data = yaml.load(infile)
|
data = yaml.load(infile)
|
||||||
|
|
||||||
return data, 200
|
return self.make_response(data)
|
||||||
|
|
||||||
|
def head(self) -> ResponseTuple:
|
||||||
|
"""Alias of GET with no response body"""
|
||||||
|
return self._head(self.get())
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
from fresnel_lens.resources._shared import FresnelResource
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailScale(FresnelResource):
|
|
||||||
|
|
||||||
routes = ("/thumb/<string:image_id>/scale/<int:scale_width>.jpg",)
|
|
||||||
|
|
||||||
def get(self, image_id: str, scale_width: int):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailResize(FresnelResource):
|
|
||||||
|
|
||||||
routes = ("/thumb/<string:image_id>/size/<int:width>x<int:height>.jpg",)
|
|
||||||
|
|
||||||
def get(self, image_id: str, width: int, height: int):
|
|
||||||
raise NotImplementedError
|
|
Loading…
Reference in New Issue
Block a user