DEV Community

XaviorCross6845
XaviorCross6845

Posted on

Property Management Classified Photos: Python 3-Stage Upload Lifecycle Quality Gate

Short answer: for classified photos, make the safe upload lifecycle a three-gate check: decode the bytes, normalize the pixels, and prove that each derivative meets your quality and bandwidth limits before publication. Keep the original quarantined, make publication a state transition, and make deletion part of the same lifecycle.

A property listing is a hostile media pipeline. A landlord may upload a phone HEIC, a huge progressive JPEG, or a file whose name says front.jpg while its bytes say something else. The useful question is not "which image service is cheapest?" It is where validation ends, who owns each copy, and what the browser is allowed to fetch.

The boundary I would put in an architecture record

The invariant is simple: no unvalidated bytes reach the public listing path. The upload endpoint accepts a bounded stream into quarantine. A worker then decodes pixels with a real image library, strips or rewrites metadata according to policy, and emits derivatives at known dimensions and formats. Only a successful validation record can move an ad from draft to published.

That boundary catches different failures than a filename check. Extension checks help user feedback, but MIME sniffing, decompression limits, and pixel dimensions are security controls. EXIF can contain GPS coordinates from a phone; orientation can make a correctly decoded image appear sideways; a valid image can still be far too large for a listing page.

Option Quality control Bandwidth behavior Failure boundary
Pass-through original Depends on uploader and browser Unpredictable; often wasteful Failures appear at render time
Client-only resize Fast feedback, weak enforcement Better on honest clients Malicious or buggy clients bypass it
Quarantine plus server derivatives Measurable and repeatable Selectable per viewport Publish waits for a worker result

I choose the third option for classified ads. It costs queue and storage design, but it gives support staff a deterministic answer when a photo is rejected.

That's the boundary.

How should classified ad photos cross a safe upload lifecycle?

Model the lifecycle as states, not as a pile of flags: received, scanning, derived, approved, published, expired, and purged. A retry must be idempotent: the same object key and content digest should not create a second public image. Keep a transition log with actor, timestamp, digest, dimensions, decoder result, and policy version. That record matters when a tenant disputes why an ad disappeared.

The critical path can stay small. The example accepts an allow-list of decoded formats, caps encoded bytes and decoded pixels, and makes publication explicit. The decode_image and make_derivative calls represent a vetted library in your runtime; they must run in a sandbox with CPU and memory limits.

from dataclasses import dataclass

MAX_UPLOAD_BYTES = 12 * 1024 * 1024
MAX_PIXELS = 40_000_000
ALLOWED_FORMATS = {"JPEG", "PNG", "WEBP"}

@dataclass
class Validation:
    state: str
    reason: str | None = None
    width: int | None = None
    height: int | None = None

def validate_and_derive(blob: bytes) -> Validation:
    if len(blob) > MAX_UPLOAD_BYTES:
        return Validation("rejected", "encoded-size")

    try:
        image = decode_image(blob)
    except DecodeError:
        return Validation("rejected", "decode")

    pixels = image.width * image.height
    if pixels > MAX_PIXELS or image.format not in ALLOWED_FORMATS:
        return Validation("rejected", "dimensions-or-format", image.width, image.height)

    normalized = normalize_orientation_and_metadata(image)
    make_derivative(normalized, width=1600, format="WEBP")
    make_derivative(normalized, width=640, format="WEBP")
    return Validation("approved", width=normalized.width, height=normalized.height)
Enter fullscreen mode Exit fullscreen mode

The limits above are policy examples, not universal truths. A rental marketplace with floor-plan scans may need a different pixel ceiling. Your mileage may vary; measure decode time and transfer size with representative phone images before tightening it. Never infer safety from a successful HTTP upload alone.

Quality and bandwidth are coupled decisions

A 1600-pixel derivative is a reasonable ceiling for a listing hero, but it is not automatically best for every screen. Generate a thumbnail for search grids, a medium image for detail pages, and retain the original only in private storage when business or legal policy requires it. Send width-aware variants with srcset and a correctly scoped sizes value so a 320-pixel card does not download a 4K source.

Compression is a perceptual decision. Compare edge halos, text on floor plans, dark interiors, and repeated brick patterns; a byte counter alone misses defects that make a property look misleading. Keep a golden corpus in tests and assert maximum bytes, dimensions, and decoder success. I use explicit budgets, such as 250 KB for a grid thumbnail and 900 KB for a detail image, then review the worst ten failures rather than averaging them away.

There is a catch: aggressive lossy compression is not suitable when a listing photo is evidence in a dispute or an accessibility workflow depends on legible signage. In those cases, preserve a policy-controlled original and select a higher-quality derivative.

Failure handling, observability, and deletion

Return a stable rejection reason to the client, but do not echo decoder internals or filesystem paths. Queue retries with a bounded attempt count and quarantine objects that repeatedly fail. Metrics should separate upload rejection, decode latency, derivative bytes, publish latency, and cleanup lag. A spike in decode rejects often means a client change; a spike in cleanup lag means the retention worker needs attention.

Deletion needs the same rigor as publication. When an ad is withdrawn, mark its media expired, remove public derivatives, and schedule private originals for policy-based purge. A nightly sweep can reconcile storage against the lifecycle table. Do not let a CDN cache turn a deleted image into a permanent copy; use short cache lifetimes for mutable paths and immutable, digest-based URLs for approved derivatives.

I am not sure any single quality metric captures "looks right" for every property type. Pair automated checks with sampled human review, document the exception path, and version the policy so old decisions remain explainable.

I rejected pass-through originals as the publication contract because it moves format, orientation, and bandwidth surprises into every consumer. It is still valid for a private archival download where the requester explicitly asks for the source file and authorization is checked. It is also useful as a quarantine artifact while derivatives are rebuilt.

It failed as a public contract.

The decision rule is practical: publish only normalized derivatives, keep provenance for every transformation, and choose quality budgets per viewing context. That makes the trade-off visible to product and compliance teams instead of hiding it in a CDN setting.

References

Top comments (0)