@@ -207,6 +207,75 @@ function ($path) use ($storage) {
207207 $ this ->assertEmpty ($ this ->listPartFiles ($ view , '' ), 'No stray part files ' );
208208 }
209209
210+ public static function expectedSizeProvider (): array {
211+ return [
212+ 'PUT with a length passes it through ' => ['PUT ' , ['CONTENT_LENGTH ' => '9 ' ], 9 ],
213+ 'PUT of an empty body still expects zero ' => ['PUT ' , ['CONTENT_LENGTH ' => '0 ' ], 0 ],
214+ 'PUT without the header expects nothing ' => ['PUT ' , [], null ],
215+ // The chunked upload assembly reaches put() as a MOVE or COPY, where the
216+ // Content-Length describes the request rather than the assembled stream.
217+ 'MOVE ignores a zero length ' => ['MOVE ' , ['CONTENT_LENGTH ' => '0 ' ], null ],
218+ 'MOVE ignores a non-zero length ' => ['MOVE ' , ['CONTENT_LENGTH ' => '9 ' ], null ],
219+ 'COPY ignores a zero length ' => ['COPY ' , ['CONTENT_LENGTH ' => '0 ' ], null ],
220+ 'COPY ignores a non-zero length ' => ['COPY ' , ['CONTENT_LENGTH ' => '9 ' ], null ],
221+ ];
222+ }
223+
224+ /**
225+ * The expected size handed to IWriteStreamStorage::writeStream() may only come from
226+ * the Content-Length of a PUT body. Passing it on for other methods makes storages
227+ * that only measure the stream when given no size - ObjectStoreStorage among them -
228+ * write a truncated or empty file.
229+ */
230+ #[\PHPUnit \Framework \Attributes \DataProvider(methodName: 'expectedSizeProvider ' )]
231+ public function testPutExpectedSizeOnlyComesFromPutContentLength (string $ method , array $ server , ?int $ expectedSize ): void {
232+ $ storage = $ this ->getMockBuilder (Local::class)
233+ ->onlyMethods (['writeStream ' ])
234+ ->setConstructorArgs ([['datadir ' => Server::get (ITempManager::class)->getTemporaryFolder ()]])
235+ ->getMock ();
236+ Filesystem::mount ($ storage , [], $ this ->user . '/ ' );
237+
238+ /** @var View&MockObject $view */
239+ $ view = $ this ->getMockBuilder (View::class)
240+ ->onlyMethods (['getRelativePath ' , 'resolvePath ' ])
241+ ->getMock ();
242+ $ view ->expects ($ this ->atLeastOnce ())
243+ ->method ('resolvePath ' )
244+ ->willReturnCallback (fn ($ path ) => [$ storage , $ path ]);
245+ $ view ->expects ($ this ->any ())
246+ ->method ('getRelativePath ' )
247+ ->willReturnArgument (0 );
248+
249+ $ receivedSize = false ;
250+ $ storage ->expects ($ this ->once ())
251+ ->method ('writeStream ' )
252+ ->willReturnCallback (function (string $ path , $ stream , ?int $ size = null ) use (&$ receivedSize ): int {
253+ $ receivedSize = $ size ;
254+ return (int )stream_copy_to_stream ($ stream , fopen ('php://temp ' , 'r+ ' ));
255+ });
256+
257+ $ info = new \OC \Files \FileInfo ('/test.txt ' , $ this ->getMockStorage (), null , [
258+ 'permissions ' => Constants::PERMISSION_ALL ,
259+ 'type ' => FileInfo::TYPE_FOLDER ,
260+ ], null );
261+
262+ $ request = new Request ([
263+ 'server ' => $ server ,
264+ 'method ' => $ method ,
265+ ], $ this ->requestId , $ this ->config , null );
266+
267+ $ file = new File ($ view , $ info , null , $ request );
268+
269+ try {
270+ $ file ->put ($ this ->getStream ('test data ' ));
271+ } catch (\Exception $ e ) {
272+ // Whatever happens after the write - size checks, renaming the part file - is
273+ // not what this test is about.
274+ }
275+
276+ $ this ->assertSame ($ expectedSize , $ receivedSize );
277+ }
278+
210279 /**
211280 * Simulate putting a file to the given path.
212281 *
0 commit comments