Expose object modification evidence

This commit is contained in:
2026-08-03 09:23:15 +02:00
parent b823a22b9b
commit bb84122061
3 changed files with 72 additions and 4 deletions
+28 -3
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from heapq import nsmallest
import os
from pathlib import Path
@@ -27,6 +28,7 @@ class StorageObjectMissing(StorageBackendError):
class StorageObjectInfo:
key: str
size_bytes: int
modified_at: datetime | None = None
@dataclass(frozen=True, slots=True)
@@ -165,9 +167,14 @@ class LocalFilesystemStorageBackend:
def stat(self, key: str) -> StorageObjectInfo:
path = self._readable_path(key)
metadata = path.stat()
return StorageObjectInfo(
key=normalize_storage_key(key),
size_bytes=path.stat().st_size,
size_bytes=metadata.st_size,
modified_at=datetime.fromtimestamp(
metadata.st_mtime,
tz=timezone.utc,
),
)
def list_objects(
@@ -188,9 +195,14 @@ class LocalFilesystemStorageBackend:
normalized_after is not None and key <= normalized_after
):
continue
metadata = path.stat()
yield StorageObjectInfo(
key=key,
size_bytes=path.stat().st_size,
size_bytes=metadata.st_size,
modified_at=datetime.fromtimestamp(
metadata.st_mtime,
tz=timezone.utc,
),
)
candidates = nsmallest(
@@ -381,7 +393,11 @@ class S3StorageBackend:
raise StorageBackendError(
"S3 object metadata did not include a valid size"
) from exc
return StorageObjectInfo(key=normalized, size_bytes=size)
return StorageObjectInfo(
key=normalized,
size_bytes=size,
modified_at=_storage_modified_at(response.get("LastModified")),
)
def list_objects(
self,
@@ -407,6 +423,7 @@ class S3StorageBackend:
StorageObjectInfo(
key=str(item["Key"]),
size_bytes=int(item.get("Size") or 0),
modified_at=_storage_modified_at(item.get("LastModified")),
)
for item in response.get("Contents", ())
if isinstance(item, dict) and item.get("Key")
@@ -418,6 +435,14 @@ class S3StorageBackend:
)
def _storage_modified_at(value: object) -> datetime | None:
if not isinstance(value, datetime):
return None
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
def configured_storage_backend(settings: object) -> StorageBackend:
"""Build the deployment-wide object store from Core settings.