diff --git a/kodak/database/_shared.py b/kodak/database/_shared.py index 9c0e813..dcef188 100644 --- a/kodak/database/_shared.py +++ b/kodak/database/_shared.py @@ -31,6 +31,17 @@ class Checksum(NamedTuple): """Construct from a hashlib object""" return cls(algorithm=data.name, digest=data.hexdigest()) + def as_header(self) -> str: + """Format the checksum for the Content-Digest HTTP header""" + if self.algorithm.startswith("sha"): + alg = f"sha-{self.algorithm[3:]}" + elif self.algorithm.startswith("id"): + alg = f"id-sha-{self.algorithm[3:]}" + else: + alg = self.algorithm + + return f"{alg}={self.digest}" + class EnumField(peewee.CharField): """Custom field for storing enums""" diff --git a/kodak/database/image.py b/kodak/database/image.py index b11bf30..3a173e6 100644 --- a/kodak/database/image.py +++ b/kodak/database/image.py @@ -38,7 +38,7 @@ class ImageRecord(KodakModel): hasher = hashlib.sha256() view = memoryview(bytearray(1024 * 1024)) with path.open("rb", buffering=0) as infile: - for chunk in iter(lambda: infile.readinto(view), 0): + for chunk in iter(lambda: infile.readinto(view), 0): # type: ignore hasher.update(view[:chunk]) name = path.stem diff --git a/kodak/resources/image.py b/kodak/resources/image.py index 26b3dee..6a9af7e 100644 --- a/kodak/resources/image.py +++ b/kodak/resources/image.py @@ -1,3 +1,8 @@ +import datetime + +import flask + +from kodak import database from kodak.resources._shared import authenticated from kodak.resources._shared import KodakResource from kodak.resources._shared import ResponseTuple @@ -9,9 +14,20 @@ class Image(KodakResource): routes = ("/image/",) @authenticated - def get(self, image_name: str) -> ResponseTuple: + def get(self, image_name: str) -> flask.Response: # pylint: disable=no-self-use """Retrieve an original source image""" - raise NotImplementedError + with database.interface.atomic(): + image = database.ImageRecord.get(database.ImageRecord.name == image_name) + + resp = flask.send_file( + image.source, + cache_timeout=int(datetime.timedelta(days=365).total_seconds()), + add_etags=False, + ) + + resp.headers["Content-Digest"] = image.checksum.as_header() + + return resp def head(self, image_name: str) -> ResponseTuple: """Alias HEAD to GET""" diff --git a/kodak/tools/index.py b/kodak/tools/index.py index 3daf30b..3431aea 100644 --- a/kodak/tools/index.py +++ b/kodak/tools/index.py @@ -127,12 +127,12 @@ def build(config: Optional[configuration.KodakConfig] = None) -> None: with database.interface.atomic(): for image in database.ImageRecord.select().where( - database.ImageRecord.deleted - == False # pylint: disable=singleton-comparison + database.ImageRecord.deleted # pylint: disable=singleton-comparison + == False ): if config.expose_source: logger.debug(f"Creating source link to {image.source}") image.create_link(config) else: logger.debug(f"Removing source link to {image.source}") - image.delete_link(config) + image.remove_link(config)