feat(file-service): accept string|resource content for streamed writes - #2184
feat(file-service): accept string|resource content for streamed writes#2184rjzondervan wants to merge 9 commits into
Conversation
Widen FileService::saveFile/addFile and the Create/UpdateFileHandler content parameter from string to string|resource (native mixed + @param string|resource, as PHP has no resource type keyword). On the resource path the handlers skip string-only base64 decoding, run the executable-file guard against a bounded magic-byte prefix (rewinding afterwards), and compute the change-detection md5 via hash_update_stream — so a synchronized file streams straight to storage via putContent() without being buffered 2-3x in memory. String callers are unaffected (pure type widening). Provider side of openconnector#110 (stream-file-content); the consumer-side fetchFile streaming lands in openconnector. Assisted-by: ClaudeCode:claude-opus-4-8
/js/ is gitignored but these two files remained tracked from earlier web-push commits; the ignore rule only prevents future re-adds. Assisted-by: ClaudeCode:claude-opus-5
… path contract.md for stream-file-content requires native `mixed` PLUS a `@param string|resource` docblock on every `$content` surface: PHP has no `resource` type keyword, so the docblock is what PHPStan/Psalm enforce at the call sites. `FileService::updateFile` and `UpdateFileHandler::updateFile` declared `@param mixed`, which silently widened the enforced contract on the update path — an int, array or object would pass static analysis there. Both are nullable (null = update tags/metadata only), so the accurate narrowing is `string|resource|null`. saveFile/addFile and both CreateFileHandler methods already matched the contract and are untouched. No runtime behaviour change: the is_resource() branching in both handlers was already correct and is covered by the existing handler tests (313 green). Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Robert Zondervan <robert@conduction.nl>
…n shape
`objectStore.massDeleteObject()` was called from three places
(MassDeleteObject.vue, SearchIndex.vue) but never defined, in neither this app's
store nor @conduction/nextcloud-vue. It threw `TypeError: not a function` before
any request, so the confirm button did nothing at all.
Add it, delegating to the package store's `deleteObjects(type, ids)` whose
`{successfulIds, failedIds}` return shape is exactly what the callers destructure.
Two follow-on defects that only became reachable once the action existed:
- Selection shape. The store holds plain id STRINGS (SearchIndex's
`selectedIdsForPage` does `list.map(String)` and binds `:selected-ids`, so that
contract cannot change), but the dialog renders objects and submitted
`map(obj => obj.id)` — undefined for every entry. The package store's
`_buildUrl(type, id)` appends `/${id}` only `if (id)`, so an undefined id
silently became a COLLECTION-scoped DELETE: HTTP 405 per selected row instead of
one delete per object. Resolve ids to rows inside the dialog (the only consumer
that needs objects), keep writing ids back to the store, and refuse in
massDeleteObject when nothing resolves so a falsy id can never target the
collection again.
- Post-delete refresh. `refreshObjectList()` refetches the register/schema
collection derived from registerStore/schemaStore, but the search view renders
`objectStore.searchCollection`, refilled only by `refetchSearchCollection()`.
Deleting from search left the deleted rows on screen. Call both, and close the
dialog immediately instead of after 2s — the selection is empty by then, so the
template fell back to an empty "no objects selected" confirmation the user had
to dismiss by hand.
Verified: per-id DELETE 204, collection DELETE 405, object total 40 -> 39.
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Robert Zondervan <robert@conduction.nl>
The dev instance runs Nextcloud 35.0.0.1 while the app declared `max-version="34"`, so `occ app:enable` refused it and the app had to be force-enabled. Verified working on 35: the app's own API endpoints answer with real payloads and no unresolved-service errors remain in the log. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Robert Zondervan <robert@conduction.nl>
…o feature/stream-file-content
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 174/174 | |||
| npm | ✅ | ✅ 555/555 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-07-29 10:07 UTC
Download the full PDF report from the workflow artifacts.
No conflicts. The branch was 166 commits behind, so its green CI was a measurement of a base that no longer exists; this makes the next run measure the change against what it would actually merge into. The merge result is exactly this branch's contribution on top of current development: the string|resource contract on the create/update file handlers and its tests, the mass-delete store action, the NC 35 max-version bump, and the two accidentally-tracked js/ push bundles staying deleted (/js/ is already gitignored and every other bundle there is already untracked). docs/features.json drops out because development has since regenerated it.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 713/713 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ |
Quality workflow — 2026-08-04 10:54 UTC
Download the full PDF report from the workflow artifacts.
|
Brought this branch current with
Cause is commit
They are not webpack entries, so nothing regenerates them. Consequences:
Suggested fix: revert Everything else on this branch (the |
Summary
$contentparameter onFileService::saveFile/addFileand theCreateFileHandler/UpdateFileHandlercontent paths fromstringtostring|resource, so a stream can be handed straight toOCP\Files\File::putContent()(which already accepts one) instead of being buffered into a PHP string. This is the provider half of stream-file-content; the OpenConnector consumer half is fix(sync): stream file downloads to disk and record the contract before file rules openconnector#1087.resourcetype keyword, so the runtime declaration ismixedand the enforced contract lives in the@param string|resourcedocblock that PHPStan/Psalm check at each call site.hash_update_stream(chunked, never buffered) and the stream rewound, so an unchanged file still skips both the write and the version bump.FileService::updateFileandUpdateFileHandler::updateFilesaid@param mixed, which silently widened the enforced contract there — an int, array or object would have passed static analysis. Both are nullable (null = update tags/metadata only), so the accurate narrowing isstring|resource|null.js/openregister-push-*.jsbundles./js/is gitignored but these stayed tracked from earlier web-push work — the ignore rule only prevents future adds.Also in this PR
Two fixes that are not part of stream-file-content but were found while verifying it, and are separable if you'd prefer them split out:
objectStore.massDeleteObject()was called from three places but never defined, in neither this app's store nor@conduction/nextcloud-vue— it threwTypeError: not a functionbefore any request, so the confirm button did nothing. Added, delegating to the package store'sdeleteObjects(type, ids). Two follow-on defects only became reachable once it existed: the store holds id strings while the dialog submittedmap(obj => obj.id)(undefined for every entry), and_buildUrl(type, id)appends/${id}onlyif (id)— so an undefined id became a collection-scoped DELETE, i.e. HTTP 405 per selected row. And the post-delete refresh calledrefreshObjectList(), which refetches a different collection than the search view renders, leaving deleted rows on screen.appinfo/info.xml.Checks
tests/Unit/Service/File/greenupdated: 5, created: 0DELETE→ 204 and collectionDELETE→ 405, and that the object total moved 40 → 39 on a mass deleteIssues
Refs ConductionNL/openconnector#1079 — this is the OpenRegister provider half of that cross-repo change, so it does not auto-close the issue.
Test plan