survos / storage-bundle
Symfony bundle for interacting with storage (via Flysystem)
Package info
github.com/survos/storage-bundle
Type:symfony-bundle
pkg:composer/survos/storage-bundle
Fund package maintenance!
Requires
- php: ^8.5
- ext-zip: *
- doctrine/doctrine-bundle: ^3.2
- doctrine/orm: ^3.7
- league/flysystem-bundle: ^3.5
- survos/data-contracts: ^2.0
- survos/jsonl-bundle: ^2.5
- survos/kit-bundle: ^2.5
- survos/tree-bundle: ^2.0
- survos/workflow-extras-bundle: ^2.43.23
- survos/workflow-messenger-bundle: ^2.43.23
- symfony/config: ^8.1
- symfony/dependency-injection: ^8.1
- symfony/http-kernel: ^8.1
- symfony/messenger: 8.2.*@dev
- symfony/security-core: ^8.1
- symfony/string: ^8.1
- twig/twig: ^3.4|^4.0
- zenstruck/bytes: ^1.0
Requires (Dev)
- league/flysystem-aws-s3-v3: ^3.29
- phpstan/phpstan: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^13.0
- survos/simple-datatables-bundle: ^2.5
- symfony/browser-kit: ^8.1
- symfony/console: ^8.1
- symfony/framework-bundle: ^8.1
- symfony/http-client: ^8.1
- symfony/phpunit-bridge: ^8.1
- symfony/stopwatch: ^8.1
- symfony/twig-bundle: ^8.1
- symfony/var-dumper: ^8.1
- symfony/yaml: ^8.1
Suggests
- survos/imgproxy-bundle: optional thumbnail and preview rendering for image objects
- survos/simple-datatables-bundle: searchable/sortable tables when browsing
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 2.45.18
- 2.45.15
- 2.45.14
- 2.45.11
- 2.45.2
- 2.42.14
- 2.41.9
- 2.39.0
- 2.37.4
- 2.34.25
- 2.33.0
- 2.24.33
- 2.24.32
- 2.24.30
- 2.24.1
- 2.19.2
- 2.18.18
- 2.10.25
- 2.10.19
- 2.10.18
- 2.10.17
- 2.10.16
- 2.10.15
- 2.10.14
- 2.10.13
- 2.10.12
- 2.10.11
- 2.10.10
- 2.10.9
- 2.10.8
- 2.10.7
- 2.10.6
- 2.10.5
- 2.10.4
- 2.10.3
- 2.10.2
- 2.10.1
- 2.10.0
- 2.9.4
- 2.9.3
- 2.9.2
- 2.9.1
- 2.9.0
- 2.8.4
- 2.8.3
- 2.8.2
- 2.8.1
- 2.8.0
- 2.7.23
- 2.7.22
- 2.7.21
- 2.7.20
- 2.7.19
- 2.7.18
- 2.7.17
- 2.7.16
- 2.7.15
- 2.7.14
- 2.7.13
- 2.7.12
- 2.7.11
- 2.7.10
- 2.7.9
- 2.7.8
- 2.7.7
- 2.7.6
- 2.7.5
- 2.7.4
- 2.7.3
- 2.7.2
- 2.7.1
- 2.7.0
- 2.6.0
- 2.5.8
- 2.5.7
- 2.5.6
- 2.5.5
- 2.5.3
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.4
- 2.4.3
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.0
- 2.2.5
- 2.2.4
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.2
- 2.1.1
- 2.0.220
- 2.0.219
- 2.0.218
- 2.0.217
- 2.0.216
- 2.0.215
- 2.0.214
- 2.0.213
- 2.0.212
- 2.0.211
- 2.0.210
- 2.0.209
- 2.0.208
- 2.0.207
- 2.0.206
- 2.0.205
- 2.0.204
- 2.0.203
- 2.0.202
- 2.0.201
- 2.0.200
- 2.0.199
- 2.0.198
- 2.0.197
- 2.0.196
- 2.0.195
- 2.0.194
- 2.0.193
- 2.0.192
- 2.0.191
- 2.0.190
- 2.0.189
- 2.0.188
- 2.0.187
- 2.0.186
- 2.0.185
- 2.0.184
- 2.0.183
- 2.0.182
- 2.0.181
- 2.0.180
- 2.0.179
- 2.0.178
- 2.0.177
- 2.0.176
- 2.0.175
- 2.0.174
- 2.0.173
- 2.0.172
- 2.0.171
- 2.0.170
- 2.0.169
- 2.0.168
- 2.0.167
- 2.0.166
- 2.0.165
- 2.0.164
- 2.0.163
- 2.0.162
- 2.0.161
- 2.0.160
- 2.0.159
- 2.0.158
- 2.0.156
- 2.0.155
- 2.0.154
- 2.0.146
- 2.0.145
- 2.0.144
- 2.0.143
- 2.0.142
- 2.0.141
- 2.0.140
- 2.0.139
- 2.0.138
- 2.0.137
- 2.0.136
- 2.0.135
- 2.0.134
- 2.0.133
- 2.0.132
- 2.0.131
- 2.0.130
This package is auto-updated.
Last update: 2026-11-01 10:59:24 UTC
README
A Symfony bundle to interact with storage (via Flysystem). It exposes commands, controllers and twig utilities. All of the underlying storage happens through Flysystem.
Used in production by
(derived from showcase's Component/Site scan — apps whose composer.json requires survos/storage-bundle and have a deployed dokku site)
Quickstart
symfony new storage-demo --webapp && cd storage-demo composer require survos/storage-bundle
Optional thumbnail support:
composer require survos/imgproxy-bundle
Without imgproxy, the bundle still works normally and falls back to direct object links instead of generated image thumbnails/previews.
Configure Flysystem, including the relevant env vars if using something besides local
# config/packages/flysystem.yaml # Read the documentation at https://github.com/thephpleague/flysystem-bundle/blob/master/docs/1-getting-started.md flysystem: storages: default.storage: adapter: 'aws' # visibility: public # Make the uploaded file publicly accessible in S3 options: client: 'Aws\S3\S3Client' # The service ID of the Aws\S3\S3Client instance bucket: '%env(AWS_S3_BUCKET_NAME)%' streamReads: true prefix: '%env(S3_STORAGE_PREFIX)%' when@dev: flysystem: storages: default.storage: adapter: 'local' options: directory: '%kernel.project_dir%/public/storage'
symfony new storage-demo --webapp && cd storage-demo composer require survos/storage-bundle bin/console storage:config bin/console storage:list
You can browse interactively with the basic admin controller.
composer require survos/simple-datatables-bundle symfony server:start -d symfony open:local --path=/storage/zones
Storage zones (and their credentials) are configured in flysystem.yaml as shown above —
local during dev, S3/Bunny/etc. in production via env vars.
To see what got wired at runtime, run the read-only diagnostic:
bin/console storage:config
It dumps the resolved zones and their adapters (class, root/bucket), which is handy for confirming env vars resolved correctly. It makes no changes and needs no credentials.
Legacy note: earlier versions shipped a Bunny-specific
storage:config <api-key>that fetched per-zone keys from the Bunny dashboard. The bundle is now backend-agnostic (Flysystem), so that key-fetcher is gone;storage:configis purely diagnostic.
Your application now has a bare-bones controller located at /admin/storage, you may want to secure this route in security.yaml, or configure it in config/routes/survos_storage.yaml.
You also have access to a command line interface.
bin/console storage:list
+------------- museado/ -----+--------+ | ObjectName | Path | Length | +----------------+-----------+--------+ | photos finales | /museado/ | 0 | +----------------+-----------+--------+
Live browsing and persisted inventory
Flysystem browsing lists the current backend directly. Doctrine inventory stores StorageNode records with parent/child relationships; tree-bundle is required for hierarchy support. Browsing a zone does not require populating its inventory.
Populate through the StorageNode workflow
Flysystem remains authoritative. Doctrine stores a disposable inventory. A StorageNode is either a directory or a file; sidecars are ordinary files.
survos_storage: tracking_lock_factory: lock.workflow.factory storages: archive.storage: track: true sidecar_prefix: o content_patterns: ['o/*.json']
bin/console storage:populate archive.storage o
# Run consumers only when ready to process the queued scan:
bin/console messenger:consume storage.fetch
Use a shared workflow lock store and configure the storage.fetch transport.
--concurrency needs Symfony Messenger's amphp/parallel support; otherwise run
multiple normal workers. sync must also be configured for explicit --sync use.
Scans default to listing-only. Each directory transition performs one shallow
Flysystem listing, records the metadata supplied by the adapter, and queues child
directories recursively. Files finish at StorageNode::STATUS_FILE_LISTED
(file_listed); they are not waiting for processing. The scanner does not queue
per-file existence, size or modification-time requests, download file contents,
or inspect archive members. fileCount counts direct files in a storage directory,
not files inside ZIPs or bags. Listing metadata can be absent when the adapter
does not supply it. Application discovery callbacks may explicitly do extra work.
To enable per-file checks and configured content caching:
bin/console storage:populate archive.storage o --load-files
With --load-files, newly discovered files use the load_file transition. It checks
existence and refreshes metadata; paths matching content_patterns are read and
cached in meta[StorageNode::CONTENT], with SHA-256 in
meta[StorageNode::CONTENT_SHA256]. It emits StorageFileLoadedEvent for application
enrichment, then finishes at file_loaded. The flag does not itself enable media
analysis or archive inspection. The same flag works with explicit --sync.
On rescans, directories are revisited. In listing-only mode, encountered files
become file_listed without refreshing previously cached contents. With
--load-files, files are queued when listing metadata changed, configured contents
are missing, or loading is unfinished. Unseen/deleted nodes are not purged by a
listing. Cached content from earlier scans must not be treated as freshly verified
merely because a node was listed again.
Recorded scan request
DirectoryScanStarter::start($zone, $path, sync: false, loadFiles: false) records
meta[StorageNode::SCAN_REQUEST] on the root and copies it to discovered children:
id: a new request identifier for each start;requestedAt: ISO 8601 request time;root: the normalized relative root path;recursive:true(traversal is always recursive);loadFiles: the requested per-file mode.
Each successfully listed directory also records meta[StorageNode::SCANNED_AT].
The command prints the mode and request ID. These fields describe the latest scan
observed by each node, not an append-only run history or a whole-tree completion
timestamp. sync is an execution scope, not a persisted scan option. Roots created
directly without the starter default to listing-only and have no generated request
ID/time. Avoid overlapping scans of the same tree with different modes: the request
metadata lives on the shared nodes.
Compatibility and application callbacks
This changes the default for all consuming applications. Callers that relied on
population to warm sidecar caches must add --load-files, or pass loadFiles: true
to DirectoryScanStarter::start(). content_patterns alone does not opt a scan in.
--dispatch/--recursive remain compatibility options; traversal and dispatch are
already enabled. --sync remains an explicit diagnostic execution scope.
An application can opt in individual files during StorageNodeDiscoveredEvent by
setting $event->node->meta[StorageNode::SCAN_REQUEST][StorageNode::LOAD_FILES] = true.
Use a listener priority above zero so this happens before NodeRefreshListener
decides whether an existing file needs loading. The node then records its effective
mode; its siblings keep the root's mode.
Directly persisted new file nodes and tracked-write journal processing still use
load_file; listing-only is a scan policy, not a prohibition on explicit file loads.
Already queued file jobs are not cancelled by changing the scan default. Inspect
and drain or selectively remove old scan jobs before a controlled listing-only run;
do not purge unrelated messages. Reports must recognize file_listed as complete
for listing-only work. The new request fields use the existing JSON meta column;
they need no new inventory table or schema migration. The separate BIGINT file-size
mapping requires consuming apps to migrate storage_node.file_size if still INTEGER.
Applications may listen for StorageNodeDiscoveredEvent during listing or
StorageFileLoadedEvent after loading, before the executor flushes. No AI-specific
listener or association to an original object is needed to cache a sidecar.
Mediary keeps one file per media ID and task, at the existing
MediaKeyService::archivePathFromKey($id, $task.'.json', 'o') path. Its
CachedSidecarReader derives that path and decodes the cached node contents.
The S3 payload is unchanged. Tasks remain independent because they are different
files. A cache miss falls back to storage; a cache hit performs no S3 request.
Tracked writes and changelog
track: true wraps the Flysystem adapter so directly injected operators and
StorageService writes are tracked alike. Successful writes, streams, moves,
copies, deletes and visibility changes append durable Doctrine journal rows.
Failed backend writes and reads append nothing. Apply the application migrations
for StorageChange and StorageJournalHead before enabling tracking. Journal rows
have no foreign keys to disposable StorageNodes. Redis is not required.
storage:changes:consume forwards journal entries to the same StorageNode workflow
and marks them dispatched. It never loads files itself. Mediary's Scheduler runs
this forwarding command every minute. A crash after dispatch can produce a safe
duplicate invalidation. Workers read the current file, so delayed messages do not
restore old content. Missing files are marked meta[StorageNode::EXISTS] = false
and their cached contents are removed. Per-node workflow locks serialize updates.
The admin-only GET /storage/changes?after=0&limit=100 endpoint returns events
and a next cursor. Without authorization it returns 403. Sequence allocation is
serialized through database commit; rows remain available after dispatch and
clients checkpoint after processing a page. There is no automatic pruning.
Filesystem writes and SQL journal inserts cannot be atomic. Journal failures are reported even if the file was saved: retry or explicitly record the successful write. Writes bypassing Flysystem require provider notifications or an explicit receipt; timestamps can detect changes on subsequent listings, but cannot prove content identity. ETags are opaque, not necessarily content hashes.
$journal->record('archive.storage', $relativeKey, revision: [ SidecarCache::ETAG => $etag, SidecarCache::VERSION_ID => $versionId, ]); // Maps confirmed bucket/key receipts through configured zone prefixes: $externalStorageWrite->record($bucket, $key, $revision);
storage:changes:record archive.storage path/to/object checks existence before
recording a manual receipt; --etag and --version-id are optional. Notification
provenance is saved under meta.storage_event_revision, separately from the hash
of the actual cached contents. Existing AI response provenance is left unchanged.
Mediary records Imagor originals only after its existing HEAD confirms persistence, including ETag/version when available. Resize HTTP success alone does not identify or confirm a persisted result object; its producer must supply a confirmed key before using the same recording API. No guessed resize keys are journaled.
Browser visibility
survos_storage.storages configures tracking (track, sidecar_prefix,
content_patterns); Flysystem still defines the storage connections. The optional
survos_storage.browse_storages list selects service IDs for the storage overview
and navbar. Omit it or use [] to show all configured stores. It is a presentation
filter, not access control: underlying services and direct storage routes remain
available.
survos_storage: browse_storages: [local_vault.storage, platform_dfolio.storage]
Friendly storage names
Flysystem service IDs and bucket/prefix configuration remain in flysystem.yaml.
Put app-specific display text in translations/storage.en.yaml:
label: 'platform_folio.storage': 'Published folios · S3' zone: 'platform_folio.storage': 'DuckDB .dfolio snapshots and legacy database artifacts.'
label.<storage-id> appears in the navbar, storage switcher and listings, falling
back to the storage ID when untranslated. Existing zone.<storage-id> keys are
descriptions, displayed on both the overview and directory page. Directory views
provide breadcrumbs, folder-first ordering and retain signed-link mode when
navigating. These labels do not rename services, buckets, prefixes or objects.
survos_storage.yaml controls tracking (track), sidecar locations
(sidecar_prefix), scanned content (content_patterns) and the tracking lock
factory. It does not define storage connections or presentation labels.