Skip to content

Add read-through filesystems for gradual storage migrations - #620

Merged
binaryfire merged 2 commits into
0.4from
filesystem-read-through
Sep 27, 2026
Merged

binaryfire merged 2 commits into
0.4from
filesystem-read-through

Conversation

@binaryfire

@binaryfire binaryfire commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

This adds a read-through filesystem driver for moving files between storage disks without taking the application offline. New files are written to the primary disk. Reads use the primary when the file is present and fall back to the old disk otherwise. By default, a fallback read also copies the file to primary; setting copy to false leaves migration to an external process.

The feature comes from laravel/framework#61140, with its follow-up changes for optional copying, copy and move operations, and visibility handling. Deletes remove files from both disks so an old fallback copy cannot reappear on the next read. Directory listings come from primary only.

Operations across the two disks are not atomic. Applications must coordinate writes and deletions to a path while it is being copied.

Adapting it to pooled connections

Hypervel's cloud disks borrow connections from a pool. An open download stream keeps its connection until the stream closes. Passing that stream directly into an upload can leave the upload waiting for a connection the download is still holding. This can happen when two disks share a pool, including disks with different buckets or prefixes, or when concurrent transfers exhaust the available connections.

Fallback transfers therefore finish reading into a temporary stream and release the source connection before borrowing the destination connection. Laravel already does this when a read copies a file to primary; Hypervel also applies it to fallback copy and move operations. Streams and borrowed connections are released on failure and coroutine cancellation. Streams returned to callers remain open until the caller closes them.

Directory listings from pooled and native cloud sides are fully loaded before being returned. This lets a caller read, copy or delete files while processing the listing without retaining the connection used to fetch it.

Resource tradeoffs

Buffering avoids holding two pooled connections at once, but it adds temporary I/O and makes the download and upload sequential. PHP's php://temp keeps small contents in memory and spills to the system temporary directory after its default memory threshold. Large transfers therefore need temporary space for the whole file. That space grows with concurrent transfers; the threshold is not a limit on total memory or temporary storage. A memory-backed temporary directory uses RAM for spilled files too.

Fully loaded listings use memory in proportion to the number of entries. Ordinary file-listing helpers already collect their results, but callers using raw listContents() must account for this behavior. With Hypervel's normal Swoole hooks, network transfers and temporary-file reads and writes yield to other coroutines while waiting for I/O; the calling coroutine still waits for its transfer to complete.

Other integration details

  • Preserve S3 and Google Cloud Storage stream options and native range reads when no fallback copying is needed, including through Sentry's storage wrapper.
  • Apply the read-through disk's configured error handling to cloud read failures.
  • Honor prefixes on both the read-through disk and its underlying disks, along with custom URL callbacks.
  • Detect circular disk definitions without sharing construction state between coroutines.
  • Keep the returned promotion stream readable when an upload adapter closes its input, including Google Cloud Storage. File uploads and fallback transfers also tolerate adapters closing their input during cleanup.
  • Use private writes by default in the shipped S3 and GCS configurations. Public visibility remains an explicit option. Copied files follow the primary disk's visibility configuration rather than inheriting source permissions.

Includes filesystem documentation and coverage for upstream behavior, shared pools, stream cleanup, cloud reads, prefixes and URL handling. The full parallel test suite, static analysis and formatting checks passed; affected tests also passed after the final corrections.

Add Laravel's read-through disk with primary writes, fallback reads,
optional promotion, deletion on both disks, fallback copy/move and
visibility handling. Port the current upstream tests and documentation.

Adapt Flysystem operations to pooled disk lifetimes. Streams retain their
leases until closed; fallback copies release the source before borrowing
the primary, allowing both sides to share a capacity-one pool. Materialize
pooled and native cloud listings before returning them. Preserve native
cloud stream options and range requests while applying the composite
disk's failure policy once, including through Sentry decorators.

Preserve outer and side prefixes for paths and URLs, honor composite URL
callbacks, reject circular construction with coroutine-local state, and
close owned resources on errors and cancellation.

Upstream:
laravel/framework#61140
laravel/framework#61155
laravel/framework#61272
laravel/framework#61375
Deletion follow-up: a833cea6ad444f64833d65f075b2a73e34b8872b
Source: laravel/framework master cd6e81dff3ba7a4564ac88c3949698c728d20109
Docs: laravel/docs 13.x eff8739e9090c2a0216fefac8e33dacdd689f8f6

Validation: full parallel suite, full static analysis and formatting pass.
After final corrections, the affected parallel suite also passes,
including pooled and non-pooled cloud reads, cleanup, Sentry storage
and generated facade checks.
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: hypervel/components/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 535ecaa4-0ad5-4ad1-86d7-f8f3dccbda89

📥 Commits

Reviewing files that changed from the base of the PR and between 2be0ccd and 76ca4df.

📒 Files selected for processing (16)
  • src/docs/filesystem.md
  • src/filesystem/README.md
  • src/filesystem/src/AwsS3V3Adapter.php
  • src/filesystem/src/ClientPooledFilesystem.php
  • src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php
  • src/filesystem/src/FilesystemManager.php
  • src/filesystem/src/FilesystemOperatorAdapter.php
  • src/filesystem/src/GoogleCloudStorageAdapter.php
  • src/filesystem/src/ReadThroughFilesystem.php
  • src/filesystem/src/ReadThroughFilesystemAdapter.php
  • src/sentry/src/Features/Storage/SentryS3V3Adapter.php
  • src/support/src/Facades/Storage.php
  • tests/Filesystem/AwsS3V3AdapterTest.php
  • tests/Filesystem/ClientPooledFilesystemTest.php
  • tests/Filesystem/FilesystemManagerTest.php
  • tests/Filesystem/FilesystemPoolProxyTest.php

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

This pull request adds read-through filesystem disks with configurable primary and fallback sides. It adds optional promotion of fallback content, pooled stream and range-read support, disk construction and URL integration, documentation, and tests.

Changes

Read-through filesystem

Layer / File(s) Summary
Pooled operators and ranged reads
src/filesystem/src/FilesystemOperatorAdapter.php, src/filesystem/src/ClientPooledFilesystem.php, src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php, src/filesystem/src/AwsS3V3Adapter.php, src/filesystem/src/GoogleCloudStorageAdapter.php, src/sentry/src/Features/Storage/SentryS3V3Adapter.php, tests/Filesystem/AwsS3V3AdapterTest.php, tests/Filesystem/FilesystemPoolProxyTest.php, src/filesystem/README.md
Adds operation-scoped operators and stream leases for pooled filesystems. S3 and Google Cloud Storage adapters expose range reads that report failures through the disk failure policy. Tests cover pooled reads, ranges, and lease release.
Primary and fallback operations
src/filesystem/src/ReadThroughFilesystemAdapter.php, tests/Filesystem/FilesystemManagerTest.php, tests/Filesystem/ClientPooledFilesystemTest.php
Adds primary-first reads and optional fallback promotion. Writes and listings target the primary; metadata and visibility operations use the disk holding the file; deletions affect both sides. Fallback moves and copies stream content to the primary. Tests cover these operations and promotion failures.
Disk construction and filesystem integration
src/filesystem/src/FilesystemManager.php, src/filesystem/src/ReadThroughFilesystem.php, src/support/src/Facades/Storage.php, src/docs/filesystem.md, tests/Filesystem/FilesystemManagerTest.php
Adds read-through disk configuration, circular-construction detection, path and URL delegation, and facade declarations. Documentation describes the configuration and disk behavior; tests cover configuration, prefixes, URLs, and callbacks.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ReadThroughFilesystemAdapter
  participant PrimaryFilesystem
  participant FallbackFilesystem
  Caller->>ReadThroughFilesystemAdapter: Read path
  ReadThroughFilesystemAdapter->>PrimaryFilesystem: Check file existence
  ReadThroughFilesystemAdapter->>FallbackFilesystem: Read when absent from primary
  ReadThroughFilesystemAdapter->>PrimaryFilesystem: Promote fallback content when copy is enabled
  ReadThroughFilesystemAdapter-->>Caller: Return file content
Loading

Merge Risk: ⚪ Minimal · up to 76ca4

This change adds read-through disks for gradual storage migrations, with fallback reads, optional copying to the primary disk, and pooled cloud stream support. No concrete merge-blocking defect has been established. One unconfirmed concern remains: promoting a file to an S3 or GCS primary disk might fail if the cloud SDK closes the temporary stream after upload. The owner may want to confirm this against a real cloud primary.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 76ca4

Read-through storage enables online migration, but concurrent operations or a failed move can make old file contents visible again. The risk is limited to disks configured for this feature; deployment permissions and external access paths are not established.

Retained concerns

  • Medium · security · inferred: A fallback promotion can overwrite a newer primary write, or recreate an object after a concurrent delete. The primary-existence recheck occurs before, rather than atomically with, the promotion write; deletion separately removes fallback then primary. This can defeat a deletion or replacement decision for an asset served through the composite disk.
  • Medium · security · inferred: A move first moves or copies the source onto primary, then deletes any fallback source. If that final delete fails or execution stops between steps, the old source remains readable through fallback after the primary source has moved. A successful sequential move is covered by a test, but it does not establish recovery from this intermediate state.
Security review details

Security Blast Radius

  • inferred — The transition risks apply to objects in each configured read-through disk, not to arbitrary storage accounts. Their external exploitability depends on which callers can read, move, write, or delete those paths and on the deployment's disk policies.

Security Findings and Attack Paths

  • inferred — A caller able to read a fallback-resident path can trigger promotion. If another operation replaces or deletes that path after the last primary-existence check, the promotion write can restore older content to primary. No unauthenticated caller or specific tenant exposure is established.
  • inferred — On failed or interrupted fallback cleanup after a move, a read or URL request for the old source path can select its remaining fallback copy. This matters where a move is intended to withdraw access to that path.

Trust Boundaries and Controls

  • observed — Separate configured operators retain their own storage scopes, and a prefix-composition test checks routing through both the composite and side prefixes. That is counterevidence to a direct path-prefix bypass, but not evidence of equivalent external authorization policies.

Resilience and Maintainability Implications

  • observed — Sequential tests verify dual-disk deletion and successful move cleanup; cancellation handling closes owned promotion streams. These controls address ordinary execution and resource lifetime, not cross-disk transition ordering under concurrency or interruption.

Hardening Proposals

  • proposed — Define a per-path migration transition protocol—such as a generation check or durable tombstone—with conditional promotion and recoverable move cleanup, so reads cannot republish superseded content and interrupted operations have an explicit recovery state.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.72% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 109 functions across 14 files. (2 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding read-through filesystems to support gradual storage migrations.
Full details: Docstring Coverage

Explanation

Docstring coverage is 69.72% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 109 functions across 14 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@binaryfire

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@binaryfire

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 27, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@binaryfire I have started the AI code review. It will take a few minutes to complete.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add read-through filesystems for gradual storage migrations

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds read-through disks for zero-downtime migrations with optional fallback promotion.
• Makes pooled cloud transfers and listings release borrowed connections safely.
• Preserves cloud ranges, prefixes, URLs, failure policies, and dual-disk deletion semantics.
Diagram

graph TD
  C["Caller"] --> R["Read-Through Disk"] --> E{"Primary file exists?"}
  E -->|Yes| P["Primary Disk"] --> S["Returned Data"]
  E -->|No| F["Fallback Disk"]
  F -->|Copy disabled| S
  F -->|Copy enabled| T["Temporary Stream"] --> P
  T --> S
  R -->|Writes and listings| P
  R -->|Deletes| P
  R -->|Deletes| F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Stream directly between disks
  • ➕ Avoids whole-file temporary storage
  • ➕ Can reduce temporary I/O for large objects
  • ➖ May deadlock when source and destination share a capacity-one pool
  • ➖ Holds two connections concurrently during transfers
  • ➖ Complicates cleanup during failures and coroutine cancellation
2. Use dedicated migration clients or pools
  • ➕ Retains direct streaming without competing for application pool capacity
  • ➕ Can improve throughput for sustained migration workloads
  • ➖ Requires additional clients, connections, and configuration
  • ➖ Does not generalize cleanly across arbitrary wrapped or inline disks
  • ➖ Increases operational resource usage
3. External migration only
  • ➕ Keeps request-time reads lightweight
  • ➕ Allows independently controlled bulk migration throughput
  • ➖ Requires a separate migration process
  • ➖ Needs coordination to prevent stale fallback files resurfacing
  • ➖ Does not automatically migrate cold files when accessed

Recommendation: Keep the temporary-stream approach as the safe default because it supports arbitrary pooled and decorated disks without requiring multiple simultaneous leases. The optional copy: false mode already provides an appropriate escape hatch for installations using an external migration process.

Files changed (16) +1613 / -50

Enhancement (10) +903 / -47
AwsS3V3Adapter.phpExpose policy-free native S3 stream reads +34/-17

Expose policy-free native S3 stream reads

• Adds a failing whole-object and ranged stream API for composite callers while retaining existing public throw and report behavior. Native S3 options and range headers remain preserved.

src/filesystem/src/AwsS3V3Adapter.php

ClientPooledFilesystem.phpProvide a lease-aware pooled filesystem operator +21/-0

Provide a lease-aware pooled filesystem operator

• Exposes an operator that borrows clients per operation, retains leases for returned streams, and preserves native S3 and GCS ranged reads.

src/filesystem/src/ClientPooledFilesystem.php

InteractsWithPooledFilesystem.phpAdd borrow-safe operator access to pooled disks +21/-0

Add borrow-safe operator access to pooled disks

• Builds a Flysystem operator around borrow-scoped operations and lease-owned streams, including validation for pooled drivers lacking raw driver access.

src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php

FilesystemManager.phpRegister and construct read-through filesystem drivers +96/-1

Register and construct read-through filesystem drivers

• Adds read-through driver validation and construction for named or inline primary and fallback disks. It also introduces coroutine-local cycle detection, pooled-side operator selection, and hyphenated driver method resolution.

src/filesystem/src/FilesystemManager.php

FilesystemOperatorAdapter.phpAdapt scoped operations into a Flysystem operator +210/-0

Adapt scoped operations into a Flysystem operator

• Introduces a full FilesystemOperator implementation that delegates each operation through a scoped callback. Stream reads use independently owned leases, native ranges are optional, and listings are materialized before releasing borrowed resources.

src/filesystem/src/FilesystemOperatorAdapter.php

GoogleCloudStorageAdapter.phpExpose policy-free native GCS stream reads +40/-29

Expose policy-free native GCS stream reads

• Adds a failing whole-object and ranged stream API while preserving configured streaming options and range headers. Existing public methods continue applying disk-level throw and report policies.

src/filesystem/src/GoogleCloudStorageAdapter.php

ReadThroughFilesystem.phpAdd the application-facing read-through filesystem +129/-0

Add the application-facing read-through filesystem

• Adds path, URL, temporary URL, upload URL, and native ranged-read behavior for the composite disk. Requests honor outer prefixes, custom callbacks, and whichever side currently contains the file.

src/filesystem/src/ReadThroughFilesystem.php

ReadThroughFilesystemAdapter.phpImplement primary-first storage migration semantics +340/-0

Implement primary-first storage migration semantics

• Implements primary writes, fallback reads, optional promotion, dual-disk deletion, visibility and metadata routing, and fallback-aware copy and move operations. Fallback streams are spooled before primary writes to avoid overlapping pooled leases and are closed safely on failures or cancellation.

src/filesystem/src/ReadThroughFilesystemAdapter.php

SentryS3V3Adapter.phpTrace policy-free S3 ranged reads +10/-0

Trace policy-free S3 ranged reads

• Forwards the new failing stream-range operation through Sentry instrumentation so wrapped S3 disks retain native read behavior.

src/sentry/src/Features/Storage/SentryS3V3Adapter.php

Storage.phpExpose read-through and operator APIs on Storage +2/-0

Expose read-through and operator APIs on Storage

• Adds facade annotations for constructing read-through drivers and retrieving borrow-safe Flysystem operators.

src/support/src/Facades/Storage.php

Tests (4) +690 / -1
AwsS3V3AdapterTest.phpTest pooled and read-through native S3 streams +126/-1

Test pooled and read-through native S3 streams

• Covers pooled whole-object streams, native read-through ranges, preserved prefixes and HTTP options, lease lifetimes, and application of the composite failure policy.

tests/Filesystem/AwsS3V3AdapterTest.php

ClientPooledFilesystemTest.phpTest shared-pool read-through resource lifetimes +71/-0

Test shared-pool read-through resource lifetimes

• Verifies fallback copying and listing processing do not retain shared capacity-one pool leases. It also confirms unpromoted caller streams retain their lease only until closure.

tests/Filesystem/ClientPooledFilesystemTest.php

FilesystemManagerTest.phpCover read-through behavior and configuration +470/-0

Cover read-through behavior and configuration

• Adds broad coverage for routing, promotion, copying, moving, deletion, visibility, URLs, prefixes, failure policies, cancellation cleanup, and circular definitions. It also verifies failed construction can be retried safely.

tests/Filesystem/FilesystemManagerTest.php

FilesystemPoolProxyTest.phpTest operator stream leases and raw failures +23/-0

Test operator stream leases and raw failures

• Confirms operator streams retain whole-driver borrows until closure and failed raw reads propagate while releasing the pooled driver.

tests/Filesystem/FilesystemPoolProxyTest.php

Documentation (2) +20 / -2
filesystem.mdDocument read-through disk configuration and behavior +18/-2

Document read-through disk configuration and behavior

• Adds read-through filesystems to the storage documentation, including primary and fallback routing, optional promotion, failure handling, listings, deletion, and visibility behavior.

src/docs/filesystem.md

README.mdDocument materialized read-through listings +2/-0

Document materialized read-through listings

• Notes that raw listings from pooled and native cloud sides are fully loaded before being returned through read-through disks.

src/filesystem/README.md

@greptile-apps

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Adds read-through filesystem driver for storage migration.

The PR does not appear safe to merge while the GCS default-visibility regression remains unresolved.

Fix All in Claude CodeFindings

  1. P1 GCS uploads become private ▶

Summary

The PR adds read-through disks for gradual storage migrations, with fallback promotion, pooled-connection-safe transfers, cloud range reads, and filesystem documentation and tests.

  • The latest changes protect returned promotion streams when upload adapters close their inputs and share native cloud reads across pool types.
  • The shipped cloud configurations no longer specify public visibility.

Reviews (3) · Last reviewed commit: "Preserve upload streams and safe cloud d..."

Comment thread src/filesystem/src/ReadThroughFilesystemAdapter.php
Comment thread src/filesystem/src/ReadThroughFilesystemAdapter.php Outdated
Comment thread src/filesystem/src/ReadThroughFilesystemAdapter.php
Comment thread src/filesystem/src/ReadThroughFilesystem.php
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Promoted private files become public 🐞 Bug ⛨ Security
Description
read() and readStream() promote fallback contents with primary->write() or
primary->writeStream() without a visibility option, so the primary disk's default replaces the
source visibility. When a private fallback file migrates to a public primary, the first read exposes
the promoted object, and fallback-based copy or move operations have the same behavior through
copyFromFallback().
Code

src/filesystem/src/ReadThroughFilesystemAdapter.php[R83-84]

+        try {
+            $this->primary->write($path, $contents);
Evidence
Both promotion paths write without configuration, while fallback copy and move only forward the
operation configuration, which normally has no visibility override. The manager passes each disk's
configured visibility to Flysystem, and the repository's supported defaults include private local
storage and public S3/GCS storage, directly demonstrating that promotion can change access controls.

src/filesystem/src/ReadThroughFilesystemAdapter.php[67-89]
src/filesystem/src/ReadThroughFilesystemAdapter.php[97-124]
src/filesystem/src/ReadThroughFilesystemAdapter.php[247-292]
src/filesystem/src/FilesystemManager.php[892-917]
src/foundation/config/filesystems.php[37-68]
src/foundation/config/filesystems.php[80-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fallback promotions, copies, and moves write to the primary without inheriting the source file's visibility, allowing private files to become public when the primary default is public.
## Fix Focus Areas
- src/filesystem/src/ReadThroughFilesystemAdapter.php[67-124]
- src/filesystem/src/ReadThroughFilesystemAdapter.php[247-292]
## Recommended Fix
Read the fallback file's visibility before promotion and pass it in the primary write configuration. For fallback copy and move operations, inherit fallback visibility when the operation configuration does not explicitly supply one, while preserving any caller-provided visibility.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. A fallback read can erase new data 🐞 Bug ≡ Correctness
Description
read() and readStream() recheck primary existence before issuing an unconditional promotion
write, leaving a yieldable check-to-write gap. If another coroutine writes a newer primary object
after that check, promotion overwrites it with legacy fallback contents, so an otherwise read-only
request destroys the new version.
Code

src/filesystem/src/ReadThroughFilesystemAdapter.php[R79-84]

+        if ($this->primary->fileExists($path)) { // @phpstan-ignore if.alwaysFalse (Fallback I/O may yield while another operation promotes the file.)
+            return $this->primary->read($path);
+        }
+
+        try {
+            $this->primary->write($path, $contents);
Evidence
The fallback read or spool, second existence check, and primary write are distinct operator calls.
FilesystemOperatorAdapter executes every such call through a separate operation closure, so pooled
network or temporary-file I/O can yield after the check and before the unconditional write.

src/filesystem/src/ReadThroughFilesystemAdapter.php[67-89]
src/filesystem/src/ReadThroughFilesystemAdapter.php[97-124]
src/filesystem/src/FilesystemOperatorAdapter.php[36-63]
src/filesystem/src/FilesystemOperatorAdapter.php[132-149]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fallback promotion performs a separate existence check and unconditional write, allowing a concurrent primary write to be overwritten with stale fallback contents.
## Fix Focus Areas
- src/filesystem/src/ReadThroughFilesystemAdapter.php[49-61]
- src/filesystem/src/ReadThroughFilesystemAdapter.php[67-124]
## Recommended Fix
Coordinate mutations and promotion for each path so the final primary existence check and promotion write cannot interleave with writes through the read-through disk. Use cancellation-safe per-path synchronization, release it in a finally block, and preserve the existing rule that an already-present primary object always wins.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Custom cloud sides fail to build 🐞 Bug ≡ Correctness
Description
FilesystemManager::readThroughOperator() calls getOperator() on every read-through side that is
not a FilesystemAdapter, even though Cloud only requires the filesystem API plus url(). A
valid ScopedCloudFilesystemProxy implements Cloud but deliberately exposes neither
getOperator() nor usable raw-driver access, so using it (or another contract-compliant custom
cloud disk) as a primary or fallback causes construction to fail with an unsupported-method error.
Code

src/filesystem/src/FilesystemManager.php[529]

+        return $disk instanceof FilesystemAdapter ? $disk->getDriver() : $disk->getOperator(); // @phpstan-ignore method.notFound (Pooled decorators forward the borrow-safe accessor.)
Evidence
The changed branch invokes getOperator() for all non-adapter cloud sides. The cloud interface does
not require this method, while the repository's scoped cloud proxy is a concrete Cloud
implementation whose unknown calls are rejected specifically to prevent prefix bypasses.

src/filesystem/src/FilesystemManager.php[519-530]
src/contracts/src/Filesystem/Cloud.php[1-13]
src/filesystem/src/ScopedCloudFilesystemProxy.php[13-38]
src/filesystem/src/ScopedFilesystemProxy.php[653-707]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
Issue description
`readThroughOperator()` assumes every non-`FilesystemAdapter` cloud disk provides `getOperator()`, but `Hypervel\Contracts\Filesystem\Cloud` does not define that method. This makes valid cloud implementations, including `ScopedCloudFilesystemProxy`, unusable as read-through primary or fallback sides.
Fix Focus Areas
- src/filesystem/src/FilesystemManager.php[519-530]
- src/filesystem/src/ScopedFilesystemProxy.php[653-707]
- src/contracts/src/Filesystem/Cloud.php[1-13]
Recommended Fix
Provide a prefix-safe read-through operator path for cloud-side implementations that do not expose raw internals, or explicitly extend and implement a supported operator capability across every supported cloud decorator/proxy. Do not call `getOperator()` based solely on the `Cloud` contract; ensure scoped and custom cloud sides can be resolved without bypassing their path boundary.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/filesystem/src/ReadThroughFilesystemAdapter.php
Comment thread src/filesystem/src/ReadThroughFilesystemAdapter.php
Comment thread src/filesystem/src/FilesystemManager.php

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 16 files

Confidence score: 2/5

  • In ReadThroughFilesystemAdapter.php, promotion can overwrite a concurrent write or recreate a deleted file, and a failed promotion can leave a corrupt primary object that later reads prefer; serialize promotion and remove partial objects before swallowing failures.
  • In ReadThroughFilesystemAdapter.php, promotion may apply the primary disk’s public visibility to a private fallback file, exposing it; preserve fallback visibility across write, stream, and copy promotions.
  • In FilesystemManager.php, the missing hypervel/context dependency can break standalone construction, while assuming every Cloud disk has getOperator() can break scoped or custom disks; declare the dependency and avoid relying on that method for all implementations.
  • In ReadThroughFilesystem.php, temporary URL support can be advertised when the selected disk cannot sign URLs, and URL generation may add a network existence check on every call; gate support on signing capability and avoid the repeated probe where possible.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/filesystem/src/FilesystemManager.php">

<violation number="1" location="src/filesystem/src/FilesystemManager.php:10">
P1: Declare `hypervel/context` in `src/filesystem/composer.json`'s `require` section; standalone installations otherwise cannot resolve `CoroutineContext` when constructing read-through disks.

(Based on your team's feedback about split-package runtime dependencies.)</violation>

<violation number="2" location="src/filesystem/src/FilesystemManager.php:529">
P2: Do not assume every `Cloud` implementation provides `getOperator()`; scoped and custom cloud disks can satisfy the contract but fail here during construction.</violation>
</file>

<file name="src/filesystem/src/ReadThroughFilesystemAdapter.php">

<violation number="1" location="src/filesystem/src/ReadThroughFilesystemAdapter.php:84">
P1: This promotion can race a primary write or delete: another coroutine can change the path after `fileExists()` returns false, then this unconditional write overwrites new contents or recreates a deleted file. Serialize promotions with writes and deletes, or use an atomic conditional promotion.</violation>

<violation number="2" location="src/filesystem/src/ReadThroughFilesystemAdapter.php:84">
P1: Preserve the fallback file's visibility on promotion writes; otherwise a primary configured with public visibility can expose a private fallback file. Apply the same inheritance to stream promotion and fallback copy/move.</violation>

<violation number="3" location="src/filesystem/src/ReadThroughFilesystemAdapter.php:86">
P1: Remove or invalidate a partially written primary object before swallowing a promotion failure; otherwise later reads prefer the corrupt object over the intact fallback.</violation>
</file>

<file name="src/filesystem/src/ReadThroughFilesystem.php">

<violation number="1" location="src/filesystem/src/ReadThroughFilesystem.php:50">
P3: `url()` and `temporaryUrl()` route through `readerFor()`, which performs a live `$this->primary->fileExists()` on every call. For S3/GCS this adds a network HEAD request per URL, and on pooled primary disks it borrows a client each time; for fallback-only files it is a HEAD on the primary plus the fallback URL lookup. Consider caching the side decision per request or documenting that URL generation costs an existence check.</violation>

<violation number="2" location="src/filesystem/src/ReadThroughFilesystem.php:58">
P2: Advertise temporary URL support only when every selected disk can sign URLs, unless the composite callback handles the request; otherwise fallback-only files can pass this check and fail in `temporaryUrl()`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

use Aws\S3\S3Client;
use Closure;
use Google\Cloud\Storage\StorageClient as GcsClient;
use Hypervel\Context\CoroutineContext;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Declare hypervel/context in src/filesystem/composer.json's require section; standalone installations otherwise cannot resolve CoroutineContext when constructing read-through disks.

(Based on your team's feedback about split-package runtime dependencies.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/filesystem/src/FilesystemManager.php, line 10:

<comment>Declare `hypervel/context` in `src/filesystem/composer.json`'s `require` section; standalone installations otherwise cannot resolve `CoroutineContext` when constructing read-through disks.

(Based on your team's feedback about split-package runtime dependencies.) </comment>

<file context>
@@ -7,6 +7,7 @@
 use Aws\S3\S3Client;
 use Closure;
 use Google\Cloud\Storage\StorageClient as GcsClient;
+use Hypervel\Context\CoroutineContext;
 use Hypervel\Contracts\Container\Container;
 use Hypervel\Contracts\Filesystem\Cloud;
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added hypervel/context as a direct filesystem dependency in c78a17d. It was already installed transitively through hypervel/coroutine, so the standalone resolution failure did not occur, but the direct declaration is appropriate.

}

try {
$this->primary->write($path, $contents);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This promotion can race a primary write or delete: another coroutine can change the path after fileExists() returns false, then this unconditional write overwrites new contents or recreates a deleted file. Serialize promotions with writes and deletes, or use an atomic conditional promotion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/filesystem/src/ReadThroughFilesystemAdapter.php, line 84:

<comment>This promotion can race a primary write or delete: another coroutine can change the path after `fileExists()` returns false, then this unconditional write overwrites new contents or recreates a deleted file. Serialize promotions with writes and deletes, or use an atomic conditional promotion.</comment>

<file context>
@@ -0,0 +1,340 @@
+        }
+
+        try {
+            $this->primary->write($path, $contents);
+        } catch (FilesystemException $exception) {
+            $this->handlePromotionFailure($path, $exception);
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The driver does not provide atomic operations across two disks. The docs now explicitly require coordinating concurrent writes and deletions while a path is copied. A worker-local lock cannot protect other workers or external migration processes; a portable atomic promotion would require a different storage contract. Keeping Laravel behavior without adding an incomplete synchronization guarantee.

try {
$this->primary->write($path, $contents);
} catch (FilesystemException $exception) {
$this->handlePromotionFailure($path, $exception);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Remove or invalidate a partially written primary object before swallowing a promotion failure; otherwise later reads prefer the corrupt object over the intact fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/filesystem/src/ReadThroughFilesystemAdapter.php, line 86:

<comment>Remove or invalidate a partially written primary object before swallowing a promotion failure; otherwise later reads prefer the corrupt object over the intact fallback.</comment>

<file context>
@@ -0,0 +1,340 @@
+        try {
+            $this->primary->write($path, $contents);
+        } catch (FilesystemException $exception) {
+            $this->handlePromotionFailure($path, $exception);
+        }
+
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not deleting the destination after a failed write. That cleanup could remove a newer valid file from another writer, and cannot identify which bytes belong to this attempt. Write publication and partial-write behavior are properties of the underlying adapter; the read-through disk does not promise rollback.

}

try {
$this->primary->write($path, $contents);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Preserve the fallback file's visibility on promotion writes; otherwise a primary configured with public visibility can expose a private fallback file. Apply the same inheritance to stream promotion and fallback copy/move.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/filesystem/src/ReadThroughFilesystemAdapter.php, line 84:

<comment>Preserve the fallback file's visibility on promotion writes; otherwise a primary configured with public visibility can expose a private fallback file. Apply the same inheritance to stream promotion and fallback copy/move.</comment>

<file context>
@@ -0,0 +1,340 @@
+        }
+
+        try {
+            $this->primary->write($path, $contents);
+        } catch (FilesystemException $exception) {
+            $this->handlePromotionFailure($path, $exception);
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the shipped public visibility overrides for S3 and GCS in c78a17d, with the matching skeleton correction. Explicit public writes still work. The docs now state that copied files use the primary disk configuration. Source ACL inheritance is not added because visibility does not map consistently across local and cloud storage and some stores do not support ACL queries.

Comment thread src/docs/filesystem.md Outdated
Comment thread src/filesystem/src/ClientPooledFilesystem.php Outdated
Comment thread src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php
);
}

return $disk instanceof FilesystemAdapter ? $disk->getDriver() : $disk->getOperator(); // @phpstan-ignore method.notFound (Pooled decorators forward the borrow-safe accessor.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not assume every Cloud implementation provides getOperator(); scoped and custom cloud disks can satisfy the contract but fail here during construction.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/filesystem/src/FilesystemManager.php, line 529:

<comment>Do not assume every `Cloud` implementation provides `getOperator()`; scoped and custom cloud disks can satisfy the contract but fail here during construction.</comment>

<file context>
@@ -434,6 +440,95 @@ public function createS3Driver(array $config): Cloud
+            );
+        }
+
+        return $disk instanceof FilesystemAdapter ? $disk->getDriver() : $disk->getOperator(); // @phpstan-ignore method.notFound (Pooled decorators forward the borrow-safe accessor.)
+    }
+
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented the supported composition. Static configured scoped disks work through configuration resolution. A dynamic scoped proxy should wrap the read-through disk, rather than be an individual side; it deliberately rejects raw internal access to protect its prefix. Arbitrary Cloud implementations do not necessarily supply the additional Flysystem and adapter capabilities this feature requires. No unsafe raw accessor was added.

*/
public function temporaryUrl(string $path, DateTimeInterface $expiration, array $options = []): string
{
return isset($this->temporaryUrlCallback)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Advertise temporary URL support only when every selected disk can sign URLs, unless the composite callback handles the request; otherwise fallback-only files can pass this check and fail in temporaryUrl().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/filesystem/src/ReadThroughFilesystem.php, line 58:

<comment>Advertise temporary URL support only when every selected disk can sign URLs, unless the composite callback handles the request; otherwise fallback-only files can pass this check and fail in `temporaryUrl()`.</comment>

<file context>
@@ -0,0 +1,129 @@
+     */
+    public function temporaryUrl(string $path, DateTimeInterface $expiration, array $options = []): string
+    {
+        return isset($this->temporaryUrlCallback)
+            ? ($this->temporaryUrlCallback)($path, $expiration, $options)
+            : $this->readerFor($path)->temporaryUrl($this->readThroughPrefixer->prefixPath($path), $expiration, $options); // @phpstan-ignore method.notFound
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping Laravel's OR behavior. This method has no path argument and reports whether signing is available, not whether every possible file can be signed. AND would incorrectly hide primary signing support when the fallback cannot sign. The per-file method selects the containing disk, and a composite callback can handle both.

*/
public function url(string $path): string
{
return $this->readerFor($path)->url($this->readThroughPrefixer->prefixPath($path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: url() and temporaryUrl() route through readerFor(), which performs a live $this->primary->fileExists() on every call. For S3/GCS this adds a network HEAD request per URL, and on pooled primary disks it borrows a client each time; for fallback-only files it is a HEAD on the primary plus the fallback URL lookup. Consider caching the side decision per request or documenting that URL generation costs an existence check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/filesystem/src/ReadThroughFilesystem.php, line 50:

<comment>`url()` and `temporaryUrl()` route through `readerFor()`, which performs a live `$this->primary->fileExists()` on every call. For S3/GCS this adds a network HEAD request per URL, and on pooled primary disks it borrows a client each time; for fallback-only files it is a HEAD on the primary plus the fallback URL lookup. Consider caching the side decision per request or documenting that URL generation costs an existence check.</comment>

<file context>
@@ -0,0 +1,129 @@
+     */
+    public function url(string $path): string
+    {
+        return $this->readerFor($path)->url($this->readThroughPrefixer->prefixPath($path));
+    }
+
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the existence check: it selects the disk that actually contains the file. Caching that decision can return a stale location during migration. The docs now include URLs among the operations that consult the containing disk without copying the file.

Keep the read-through promotion stream open when an upload adapter closes
its input, including the Google Cloud Storage client. Give uploads a
separate resource handle and detach its wrapper after use. Close streams
conditionally in fallback copy/move and putFileAs so a completed upload
cannot fail during cleanup.

Use one pooled filesystem operator implementation for both client and
whole-driver pools. Preserve native cloud reads, range requests, HTTP
options and stream leases without applying the inner disk's error policy.
Declare the filesystem package's direct context dependency.

Remove the shipped public visibility overrides for S3 and GCS, keeping
private adapter defaults and explicit public opt-in. Clarify destination
visibility, dynamic scoping, concurrent mutation and promotion failure
semantics in the read-through documentation.

Extend coverage for closing upload adapters, actual Google client stream
ownership with a fake HTTP transport, whole-driver cloud ranges and leases,
shipped-config write ACLs, and cancellation cleanup. Keep these corrections
together because their regression cases share complete test files.

Follow-up to laravel/framework#61140
Source: laravel/framework master cd6e81dff3ba7a4564ac88c3949698c728d20109

Validation: changed test files and affected parallel filesystem, storage,
Sentry, facade and foundation suites pass. Full static analysis, formatting,
facade regeneration and diff checks pass.
'storage_api_uri' => env('GOOGLE_CLOUD_STORAGE_API_URI', null), // see: Public URLs below
'api_endpoint' => env('GOOGLE_CLOUD_STORAGE_API_ENDPOINT', null), // set storageClient apiEndpoint
'visibility' => 'public', // optional: public|private
'visibility_handler' => null, // optional: set to \League\Flysystem\GoogleCloudStorage\UniformBucketLevelAccessVisibility::class to enable uniform bucket level access

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 GCS uploads become private When an application uses the shipped GCS disk without specifying visibility, removing its public setting makes ordinary writes create private objects. Applications that relied on those files being publicly accessible will find that their existing public URLs cannot retrieve new uploads. Preserve the prior default or provide an explicit migration path.

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default change is intentional. Hypervel 0.4 is unreleased, so preserving earlier application behavior is not a compatibility requirement. Cloud files should be private unless the application explicitly requests public visibility. The existing visibility option and public-write APIs remain available, and the shipped configuration tests cover both the private default and an explicit public override. We will keep the private default; no migration layer is needed.

@binaryfire

Copy link
Copy Markdown
Member Author

@coderabbitai @greptile-apps @cubic-dev-ai @qodo-free-for-open-source-projects

The latest changes:

  • Keep the promotion stream readable when an upload adapter closes its input, including Google Cloud Storage. Fallback transfers and file uploads also handle adapters that close their streams during upload.
  • Share native cloud read and range handling between both pool types, preserving request options and connection lifetimes.
  • Remove public visibility overrides from the shipped S3/GCS defaults. Public writes remain opt-in; copied files follow the primary disk configuration.
  • Declare the direct context dependency and clarify promotion failures, dynamic scoped disks, URL selection and concurrent file changes in the docs.

Regression coverage includes real Google client uploads with a fake HTTP transport, stream cleanup, both pool types, and private/default versus explicit public writes.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

@coderabbitai @greptile-apps @cubic-dev-ai @qodo-free-for-open-source-projects

The latest changes:

  • Keep the promotion stream readable when an upload adapter closes its input, including Google Cloud Storage. Fallback transfers and file uploads also handle adapters that close their streams during upload.
  • Share native cloud read and range handling between both pool types, preserving request options and connection lifetimes.
  • Remove public visibility overrides from the shipped S3/GCS defaults. Public writes remain opt-in; copied files follow the primary disk configuration.
  • Declare the direct context dependency and clarify promotion failures, dynamic scoped disks, URL selection and concurrent file changes in the docs.

Regression coverage includes real Google client uploads with a fake HTTP transport, stream cleanup, both pool types, and private/default versus explicit public writes.

Thanks for the update. The changes cover the key risks: stream ownership during promotion and uploads, pooled native cloud reads, private-by-default cloud writes, and scoped-disk/concurrency behavior. The added regression coverage appears aligned with those cases. I don’t have any Qodo findings to resolve on this PR.

@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

@binaryfire, thanks for the update. I’ll review the latest changes, with attention to stream cleanup, pooled reads, visibility defaults, and the regression tests. I have not verified those changes yet.

❌ Action failed

Review failed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps

greptile-apps Bot commented Sep 27, 2026

Copy link
Copy Markdown

Thanks for the update. The changes address the previously raised risks around stream ownership during promotion and uploads, pooled native cloud reads, private-by-default cloud writes, and scoped-disk and concurrency behavior. The added regression coverage matches those cases, so I have no further findings to resolve on this PR.

@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@binaryfire, thanks for the update. I have not verified the latest changes. The previous manual review attempt failed because the review command does not re-review already reviewed commits unless automatic reviews are paused. If you want another manual review, pause automatic reviews and request @coderabbitai review again.


✏️ Learnings added
Learnt from: binaryfire
URL: https://github.com/hypervel/components/pull/620

Timestamp: 2026-09-27T07:03:26.443Z
Learning: For the read-through filesystem in hypervel/components, the shipped S3 and Google Cloud Storage disk defaults do not override visibility to public. Public writes are opt-in, and promoted files follow the primary disk configuration.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@binaryfire
binaryfire merged commit 174ce4f into 0.4 Sep 27, 2026
39 checks passed
@binaryfire
binaryfire deleted the filesystem-read-through branch September 27, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant