Lazy: each read_bytes() issues a fresh HTTP GET (cached on disk
if cache_dir is non-NULL). list_dirs() uses S3
ListObjectsV2 with the slash delimiter. Once a store is opened, the
full object listing is fetched up front and memoised; subsequent
exists() / list_dirs() checks hit the memoised manifest.