Jul 29 - Photo stamping files
This commit is contained in:
+46
-5
@@ -47,15 +47,31 @@ def upload_photo():
|
||||
---------------------
|
||||
file — binary image data (jpg / png / gif)
|
||||
entity_type — "inspection" | "issue" | "issue_result" (controls subfolder)
|
||||
captured_at — OPTIONAL ISO-8601 capture time (e.g. 2026-07-20T09:14:22-04:00)
|
||||
latitude — OPTIONAL decimal degrees at capture
|
||||
longitude — OPTIONAL decimal degrees at capture
|
||||
|
||||
A capture-time + geo overlay is burned into the image before it is stored
|
||||
(see app/utils/photo_stamp.py). Metadata is taken from the client fields
|
||||
above, falling back to the image's EXIF, then to server receipt time.
|
||||
Sending captured_at/latitude/longitude is strongly preferred for an
|
||||
offline-first client: a photo taken at 09:14 but synced at 16:00 would
|
||||
otherwise be stamped with the sync time.
|
||||
|
||||
Response 200
|
||||
------------
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"server_path": "uploads/inspection_photos/abc123.jpg"
|
||||
"server_path": "uploads/inspection_photos/abc123.jpg",
|
||||
"stamped": true,
|
||||
"captured_at": "2026-07-20T09:14:22",
|
||||
"capture_source": "client"
|
||||
}
|
||||
}
|
||||
|
||||
The three stamp keys are additive — an iPad build that predates them decodes
|
||||
explicit CodingKeys and ignores what it doesn't know.
|
||||
"""
|
||||
user = g.api_user
|
||||
|
||||
@@ -85,12 +101,37 @@ def upload_photo():
|
||||
else:
|
||||
subfolder = 'inspection_photos'
|
||||
|
||||
# Burn the capture-time + geo overlay before the bytes are ever stored, so
|
||||
# exactly one (already-stamped) object is written and nothing has to be
|
||||
# read back out of R2. Any stamping failure returns the original bytes.
|
||||
meta = {'stamped': False, 'captured_at': None, 'source': None}
|
||||
if current_app.config.get('PHOTO_STAMP_ENABLED', True):
|
||||
from app.utils.photo_stamp import stamp_file_storage
|
||||
file_obj, meta = stamp_file_storage(
|
||||
file_obj,
|
||||
captured_at = request.form.get('captured_at'),
|
||||
latitude = request.form.get('latitude'),
|
||||
longitude = request.form.get('longitude'),
|
||||
)
|
||||
|
||||
# Write via the active storage backend (local disk or R2). Key format
|
||||
# 'uploads/<subfolder>/<uuid>.<ext>' is unchanged across backends.
|
||||
# 'uploads/<subfolder>/<uuid>.<ext>' is unchanged across backends. The
|
||||
# stamped FileStorage keeps the original filename, so the derived key — and
|
||||
# the tenant prefix applied inside S3Backend — are unaffected.
|
||||
from app.utils import storage
|
||||
server_path = storage.save(file_obj, subfolder)
|
||||
|
||||
logger.info('API PHOTOS | uploaded | entity_type=%s | path=%s | user=%s',
|
||||
entity_type, server_path, user.username)
|
||||
logger.info(
|
||||
'API PHOTOS | uploaded | entity_type=%s | path=%s | user=%s | '
|
||||
'stamped=%s | capture_source=%s',
|
||||
entity_type, server_path, user.username,
|
||||
meta.get('stamped'), meta.get('source'),
|
||||
)
|
||||
|
||||
return api_ok({'server_path': server_path})
|
||||
captured_at = meta.get('captured_at')
|
||||
return api_ok({
|
||||
'server_path': server_path,
|
||||
'stamped': bool(meta.get('stamped')),
|
||||
'captured_at': captured_at.isoformat() if captured_at else None,
|
||||
'capture_source': meta.get('source'),
|
||||
})
|
||||
Reference in New Issue
Block a user