Skip to content

fix(files_external): propagate child copy failures in AmazonS3::copy() - #63123

Open
rnixx wants to merge 1 commit into
nextcloud:masterfrom
rnixx:rnixx-fix-s3-dir-copy-error-propagation
Open

fix(files_external): propagate child copy failures in AmazonS3::copy()#63123
rnixx wants to merge 1 commit into
nextcloud:masterfrom
rnixx:rnixx-fix-s3-dir-copy-error-propagation

Conversation

@rnixx

@rnixx rnixx commented Aug 10, 2026

Copy link
Copy Markdown

Summary

When copying a directory, copy() discarded the return values of its recursive calls and always returned true. rename() relies on that value, so a failed copy still led to rmdir() on the source: every file in the directory was deleted, the destination stayed empty, and the UI reported success.

This is reachable whenever a provider rejects CopyObject for objects it otherwise serves — Hetzner Object Storage answers 501 NotImplemented for SSE-C encrypted objects, which makes every single child copy fail.

Behaviour with the fix (measured on 34.0.1, Hetzner Object Storage + SSE-C)

Renaming a folder now fails visibly instead of destroying its contents:

  • Web UI: the server answers 500, the files stay where they are. The
    destination directory created by mkdir() before the failure remains as an
    empty folder and can be deleted normally.
  • Desktop client: the MOVE fails with 500, and the client rolls the
    local rename back and re-fetches the files. It does not fall back to
    upload+delete, so nothing is duplicated and nothing is lost.
  • Upload, download and delete are unaffected.

Renaming remains impossible on such providers — that part is outside
Nextcloud's control. What changes is that the failure is now visible instead
of silently deleting the data.

Related

Previously reported without an issue:

https://help.nextcloud.com/t/nc-copy-move-not-working-and-destroying-files-in-s3-external-storage-with-sse-c-key/231896

(NC 31.0.8, same provider, same 501 NotImplemented, same silent data loss)

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@rnixx
rnixx requested a review from a team as a code owner August 10, 2026 13:58
@rnixx
rnixx requested review from Altahrim, leftybournes, provokateurin and sorbaugh and removed request for a team August 10, 2026 13:58
Comment thread apps/files_external/lib/Lib/Storage/AmazonS3.php Outdated
Comment thread apps/files_external/tests/Storage/Amazons3CopyTest.php Outdated
Comment thread apps/files_external/tests/Storage/Amazons3CopyTest.php Outdated
Comment thread apps/files_external/tests/Storage/Amazons3CopyTest.php
Comment thread apps/files_external/tests/Storage/Amazons3CopyTest.php Outdated
When copying a directory, copy() discarded the return values of its
recursive calls and always returned true. rename() relies on that value,
so a failed copy still led to rmdir() on the source: every file in the
directory was deleted, the destination stayed empty, and the UI reported
success.

This is reachable whenever a provider rejects CopyObject for objects it
otherwise serves — Hetzner Object Storage answers 501 NotImplemented for
SSE-C encrypted objects, which makes every single child copy fail.

Signed-off-by: Robert Niederreiter <rnix@squarewave.at>
@rnixx
rnixx force-pushed the rnixx-fix-s3-dir-copy-error-propagation branch from 23c1a82 to 964d9a5 Compare August 11, 2026 04:49
@rnixx

rnixx commented Aug 11, 2026

Copy link
Copy Markdown
Author

Thanks for the review — comments shortened as suggested.

For the test setup I could only apply invokePrivate() to two of the four
lines; the other two run into limitations of the helper:

  • 'test': AmazonS3::test() exists as a public method, and invokePrivate()
    checks hasMethod() before hasProperty(). It calls the method instead of
    setting the property, failing with
    Typed property AmazonS3::$params must not be accessed before initialization.
  • 'logger': the property is private on AmazonS3, so it is invisible on the
    PHPUnit mock subclass that invokePrivate() reflects on — hasProperty()
    returns false and the helper silently sets nothing.

initCaches and storageClass work and now use the helper. The remaining two
keep ReflectionProperty with a short comment explaining why.

@rnixx
rnixx requested a review from CarlSchwan August 11, 2026 09:07
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.

2 participants