1
0
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:
Ethan Paul 2021-10-28 23:07:13 -04:00
parent b61ef4624b
commit 99d2ca4816
No known key found for this signature in database
GPG Key ID: D0E2CBF1245E92BF
9 changed files with 52 additions and 167 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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,
) )

View File

@ -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

View 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

View 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())

View File

@ -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

View File

@ -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())

View File

@ -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