From d9eeb730d2e01a675601164fdb87427d29c62c8a Mon Sep 17 00:00:00 2001 From: Jared Date: Wed, 27 May 2026 13:03:19 +0400 Subject: [PATCH] docs: complete Storage package docs for v3.0 --- v3.0/packages/storage/adapters.md | 8 +++++--- v3.0/packages/storage/architecture.md | 10 ++++++---- v3.0/packages/storage/contracts.md | 28 ++++++++++++++++----------- v3.0/packages/storage/usage.md | 25 ++++++++++++++++++++---- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/v3.0/packages/storage/adapters.md b/v3.0/packages/storage/adapters.md index 29aaeea..c154c42 100644 --- a/v3.0/packages/storage/adapters.md +++ b/v3.0/packages/storage/adapters.md @@ -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 @@ -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. @@ -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 @@ -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. diff --git a/v3.0/packages/storage/architecture.md b/v3.0/packages/storage/architecture.md index 56cdc16..dc28bc5 100644 --- a/v3.0/packages/storage/architecture.md +++ b/v3.0/packages/storage/architecture.md @@ -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 diff --git a/v3.0/packages/storage/contracts.md b/v3.0/packages/storage/contracts.md index 18ee6e9..e87182a 100644 --- a/v3.0/packages/storage/contracts.md +++ b/v3.0/packages/storage/contracts.md @@ -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 diff --git a/v3.0/packages/storage/usage.md b/v3.0/packages/storage/usage.md index 4d19f08..420d9e4 100644 --- a/v3.0/packages/storage/usage.md +++ b/v3.0/packages/storage/usage.md @@ -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 @@ -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.