fix(dav): only derive the write size from a PUT Content-Length - #63510
Conversation
f0b25ca to
dafce49
Compare
put() is not only reached by PUT. The chunked upload assembly step is a MOVE or COPY of <upload>/.file: Directory::moveInto() and ::copyInto() only short-circuit File and Directory sources at storage level, so a FutureFile falls through to Tree::copyNode() -> createFile() -> put(), where the data is the AssemblyStream of the uploaded chunks and the request itself carries no body at all. For those requests the Content-Length says nothing about the stream being written. Clients that send "Content-Length: 0" there, Safari among them, made File::put() hand a 0 to IWriteStreamStorage::writeStream(). Storages that only measure the stream when they are given no size, ObjectStoreStorage among them, then wrote an empty file while the request still answered 201, so the upload looked successful and the file was silently empty. The size comparison further down already restricts itself to PUT for exactly this reason. Apply the same restriction when deriving the expected size, which covers both the MOVE and the COPY variant. Genuine empty PUT uploads are unaffected: the method is still PUT, so a Content-Length of 0 is passed through as before. Signed-off-by: mostafa <mostafakhaki00@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dafce49 to
7a4714a
Compare
|
/backport to stable35 |
|
/backport to stable34 |
|
/backport to stable33 |
|
/backport to stable34 |
|
The backport to # Switch to the target branch and update it
git checkout stable34
git pull origin stable34
# Create the new backport branch
git checkout -b backport/63510/stable34
# Cherry pick the change from the commit sha1 of the change against the default branch
# This might cause conflicts, resolve them
git cherry-pick 7a4714ad
# Push the cherry pick commit to the remote repository and open a pull request
git push origin backport/63510/stable34Error: Failed to create pull request: Validation Failed: {"resource":"PullRequest","code":"custom","message":"A pull request already exists for nextcloud:backport/63510/stable34."} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request Learn more about backports at https://docs.nextcloud.com/server/stable/go.php?to=developer-backports. |
|
/backport to stable32 |
|
Hello there, We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 Thank you for contributing to Nextcloud and we hope to hear from you soon! (If you believe you should not receive this message, you can add yourself to the blocklist.) |
MOVEwithContent-Length: 0results in 0bytes files in Object Storage #63504Summary
File::put()takes the expected write size from the request'sContent-Length, but it is not reached only byPUT. The chunked upload assembly step is aMOVE(orCOPY) of<upload>/.file:Directory::moveInto()andDirectory::copyInto()only short-circuitFileandDirectorysources at storage level, so aFutureFilefalls through toTree::copyNode()→Directory::createFile()→File::put(). There the data is theAssemblyStreamof the uploaded chunks and the request itself carries no body at all, so itsContent-Lengthdescribes nothing relevant.Clients that send
Content-Length: 0on that request — Safari does, Chrome omits the header entirely — make$expected0instead ofnull.ObjectStoreStorage::writeStream()only measures the stream when it is given no size:so the
fstat()fallback is skipped,$stat['size']becomes0, and the file is recorded as empty while the request still answers201. The upload looks successful and the file is silently empty.The size comparison further down in the same method is already restricted to
PUTfor exactly this reason:This applies the same restriction when deriving the expected size in the first place. Because the guard is on the method rather than on the value, it covers the
COPYvariant as well asMOVE— a fix that special-cased a zero length, or onlyMOVE, would not.Genuine empty
PUTuploads are unaffected: the method is stillPUT, so aContent-Lengthof0is passed through exactly as before.Scope
Object-storage primary storage is affected.
OC\Files\Storage\Local::writeStream()ignores the$sizeargument entirely and just callsfile_put_contents(), so local-storage installs should not be.Methods that reach
File::put():put()Content-LengthmeaningfulPUTCorePlugin::httpPutMOVEof<upload>/.fileAssemblyStreamCOPYof<upload>/.fileAssemblyStreamMOVE/COPYof a normal fileTesting
FileTest::testPutExpectedSizeOnlyComesFromPutContentLengthcovers the size handed towriteStream()forPUT(with a length, with0, and with the header absent) and forMOVE/COPY(with0and with a non-zero length).Against
masterwithout the change, 4 of the 7 cases fail:With the change,
Tests: 7, Assertions: 21, all passing.Verified end to end as well, on 33.0.7 with nginx + php-fpm and S3 (MinIO) primary storage, using a 102 395 904 byte file in five chunks. Adding
Content-Length: 0to the assembly request is the only variable that breaks it, for bothMOVEandCOPY, sequentially or in parallel.Optional hardening, not included here
@vpecinka independently diagnosed this in #7995 (comment) and patched the storage layer instead, forcing
$size = nullinObjectStoreStorage::writeStream()when a zero is announced for a stream thatfstat()shows is larger. That defends against any other caller passing a bogus zero, which this change does not.It is deliberately left out to keep this PR to the root cause;
writeStream()putting the announced size straight into the filecache is arguably worth guarding on its own. Happy to add it here or as a separate PR if you would prefer it.Checklist
php-cs-fixerreports no changes for both files)3. to review, feature component)stable32)AI (if applicable)