b0f83cfa34
Request-path thumbnail generation (REST get_thumbnail and NC preview) read the full source blob into RAM and decoded it with no concurrency bound — a first-view gallery of K images stacked K full-size buffers and K parallel decodes on the blocking pool. The background hook had the inverse ordering problem: it read the blob eagerly and only then queued on the decode semaphore, so N concurrent uploads held N originals in memory while waiting. All three paths now acquire the decode semaphore first and read the blob under the permit, capping peak RAM at permits x image size: - new ThumbnailService::get_thumbnail_from_blob defers the blob read into the moka init closure (cache and disk hits never touch the blob); both handlers use it and no longer pre-read. - get_thumbnail_from_bytes (direct-bytes variant) now also takes a permit before decoding; shared generate_and_persist core keeps the two entrypoints duplicate-free. - generate_all_sizes_background_from_blob (renamed from _from_bytes) checks blob existence and disk-dedup state, then acquires the permit, then reads; the redundant pre-read spawn wrapper in ThumbnailRefreshHook is gone. https://claude.ai/code/session_01QxwJDHqQhbMkHK333QtMme