Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions v3.0/packages/storage/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Dropbox operations use path-like names such as `exports/report.csv`. The adapter
- `put()` always uploads with overwrite mode
- `append()` is implemented as read-then-write, so it is not an atomic append operation
- `exists()` delegates to `isFile()`, so directory checks still require `isDirectory()`
- most adapter failures are converted to `false` instead of being rethrown directly
- most adapter-level request errors come back as `false` instead of being rethrown directly

## Google Drive adapter

Expand All @@ -66,6 +66,7 @@ Google Drive is less path-oriented than Dropbox.

- `makeDirectory('Invoices')` creates a folder by name under `root` unless you pass a parent ID
- `put('invoice.pdf', $content, $parentId)` creates a new file by name under a parent folder ID
- if the first `put()` argument already matches an existing file ID, the adapter writes into that Drive file instead of creating a new one
- once a file exists, most later operations (`get()`, `rename()`, `remove()`, `copy()`, `isFile()`, `isDirectory()`) expect a Google Drive file or folder ID rather than a path

Treat that adapter as ID-based after creation.
Expand All @@ -75,7 +76,8 @@ Treat that adapter as ID-based after creation.
- `copy($source, $dest)` copies a file ID into a destination folder ID, defaulting to `root`
- `append()` is also read-then-write rather than atomic
- `listDirectory($dirname)` expects a folder ID and returns the Drive API `files` list for that parent
- adapter failures are returned as `false`
- `UploadedFile::save()` is best reserved for path-style remote adapters; direct `fs('gdrive')->put(..., $parentId)` calls give you explicit Drive folder placement
- adapter-level request errors are returned as `false`

## Cloud auth requirements

Expand All @@ -87,4 +89,4 @@ That service must implement `Quantum\Storage\Contracts\TokenServiceInterface` so
- get the refresh token
- save new tokens after the first OAuth exchange or an automatic refresh

If the service does not implement that contract, adapter resolution fails before any file operation runs.
When the configured service implements another interface, adapter resolution raises a storage exception before file operations begin.
10 changes: 6 additions & 4 deletions v3.0/packages/storage/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ Unsupported adapter methods are rejected with exceptions instead of being ignore
`UploadedFile` runs this sequence:

1. parse upload metadata
2. detect extension + MIME from local temp file
2. detect extension + MIME from the local temp file
3. load MIME policy (defaults + optional config)
4. validate upload and destination
5. write locally or through remote adapter
6. optionally run post-save image modification
4. validate the temp file and destination strategy
5. write locally or forward the temp file content to a remote adapter
6. optionally run post-save image modification against the saved path

Practical effect: upload inspection and staging still depend on the local adapter, even when the final destination is remote.

## Cloud composition

Expand Down
28 changes: 17 additions & 11 deletions v3.0/packages/storage/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,35 @@ Use them only when the adapter is known to be local.

## Upload validation contract

`UploadedFile::save()` enforces this flow:
`UploadedFile::save()` applies this flow:

1. upload error must be `UPLOAD_ERR_OK`
2. temp file must exist
3. MIME+extension must pass policy
4. local destination must be valid (exists, writable)
5. overwrite is blocked unless explicitly enabled
1. upload status is read from the PHP upload metadata
2. the temp file path is verified through the local adapter
3. MIME+extension is matched against the active policy
4. local destinations are prepared through directory and overwrite checks
5. the file is written locally or forwarded to the configured remote adapter

Any failed check throws before file write.
Validation exceptions are raised before persistence when one of those checks does not pass.

## MIME policy contract

`UploadedFile` starts from built-in MIME rules, then merges `uploads.allowed_mime_types` from config.
`UploadedFile` starts from built-in MIME rules, then merges `uploads.allowed_mime_types` from config when that file is present.

Runtime overrides are supported via `setAllowedMimeTypes()`.
Runtime overrides are supported via `setAllowedMimeTypes()`. With the default `merge: true`, your additions are layered on top of the built-in rules. Pass `merge: false` when you want the runtime policy to become the full allow-list for that instance.

## Remote upload contract

When a remote adapter is attached with `setRemoteFileSystem(...)`:

- local destination checks are skipped
- upload payload is streamed from local temp file
- destination string is interpreted according to the remote adapter
- the upload payload is still read from the PHP temp file through the local adapter
- the destination string is interpreted according to the remote adapter

That makes path-based adapters straightforward with `UploadedFile::save()`. For Google Drive parent-folder placement, the adapter-level `put($filename, $content, $parentId)` call is the clearer fit.

## Non-HTTP source contract

`UploadedFile::save()` also works with files constructed from non-upload temp paths in tests and CLI code. In that case, the storage layer copies the source file into the destination instead of using PHP's upload move path.

## Cloud token contract

Expand Down
25 changes: 21 additions & 4 deletions v3.0/packages/storage/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ $uploadedFile->setRemoteFileSystem(fs('dropbox')->getAdapter());
$uploadedFile->save('reports/2026');
```

This flow is a natural fit for path-based adapters such as Dropbox. During the save, Quantum still reads the PHP temp file through the local adapter before it writes to the remote adapter, so keep the default `fs()` adapter on `local` for upload-heavy flows.

For Google Drive folder placement, use the filesystem adapter directly when you need to pass a parent folder ID:

```php
$uploadedFile = new UploadedFile($_FILES['report']);

fs('gdrive')->put(
$uploadedFile->getNameWithExtension(),
file_get_contents($uploadedFile->getPathname()),
$folderId
);
```

`UploadedFile::save()` builds one destination string, while the Google Drive adapter accepts the parent folder as a separate argument.

## Modify images after save

```php
Expand All @@ -67,7 +83,8 @@ $uploadedFile

## Common pitfalls

- Destination directory must exist and be writable for local saves.
- MIME and extension must match policy.
- `getDimensions()` is image-only and throws for invalid image files.
- Post-save image modifications are not ideal for direct cloud uploads.
- Destination directory should already exist and be writable for local saves.
- MIME and extension should match the active upload policy.
- `getDimensions()` is image-focused and throws when the temp file is not a readable image.
- Post-save image modifications fit local destinations best, because the modifier runs against the saved path after persistence.
- In tests and CLI scripts, `save()` copies the source path into place when the file did not come through PHP's HTTP upload pipeline.