@@ -71,6 +71,11 @@ private function isRoot(string $path): bool {
7171 return $ path === '. ' ;
7272 }
7373
74+ private function getCachePath (string $ path ): string {
75+ // normalizePath() converts '' to '.' for S3 object keys, but filecache stores the root as ''
76+ return $ this ->isRoot ($ path ) ? '' : $ path ;
77+ }
78+
7479 private function cleanKey (string $ path ): string {
7580 if ($ this ->isRoot ($ path )) {
7681 return '/ ' ;
@@ -325,6 +330,28 @@ public function stat(string $path): array|false {
325330 return $ stat ;
326331 }
327332
333+ #[\Override]
334+ public function getMetaData (string $ path ): ?array {
335+ $ data = parent ::getMetaData ($ path );
336+ if ($ data !== null && $ data ['mimetype ' ] === FileInfo::MIMETYPE_FOLDER ) {
337+ // Common::getMetaData sets storage_mtime = mtime, but for S3 virtual directories
338+ // mtime may have been updated by mtime propagation while storage_mtime should
339+ // reflect the actual last storage change. Without this override the scanner sees
340+ // data['storage_mtime'] != cacheData['storage_mtime'] and re-writes the cache,
341+ // causing View::getCacheEntry to trigger propagateChange on every read.
342+ $ path = $ this ->normalizePath ($ path );
343+ $ cacheEntry = $ this ->getCache ()->get ($ this ->getCachePath ($ path ));
344+ if ($ cacheEntry instanceof CacheEntry) {
345+ $ data ['storage_mtime ' ] = $ cacheEntry ->getStorageMTime ();
346+ } elseif (!$ this ->isRoot ($ path ) && $ directoryMarker = $ this ->headObject ($ path . '/ ' )) {
347+ if (isset ($ directoryMarker ['LastModified ' ])) {
348+ $ data ['storage_mtime ' ] = strtotime ($ directoryMarker ['LastModified ' ]);
349+ }
350+ }
351+ }
352+ return $ data ;
353+ }
354+
328355 #[\Override]
329356 public function is_dir (string $ path ): bool {
330357 $ path = $ this ->normalizePath ($ path );
@@ -648,12 +675,14 @@ public function getDirectoryContent(string $directory): \Traversable {
648675 }
649676
650677 private function objectToMetaData (array $ object ): array {
678+ $ mtime = isset ($ object ['LastModified ' ]) ? strtotime ($ object ['LastModified ' ]) : time ();
679+ $ etag = isset ($ object ['ETag ' ]) ? trim ($ object ['ETag ' ], '" ' ) : '' ;
651680 return [
652681 'name ' => basename ($ object ['Key ' ]),
653682 'mimetype ' => $ this ->mimeDetector ->detectPath ($ object ['Key ' ]),
654- 'mtime ' => strtotime ( $ object [ ' LastModified ' ]) ,
655- 'storage_mtime ' => strtotime ( $ object [ ' LastModified ' ]) ,
656- 'etag ' => trim ( $ object [ ' ETag ' ], ' " ' ) ,
683+ 'mtime ' => $ mtime ,
684+ 'storage_mtime ' => $ mtime ,
685+ 'etag ' => $ etag ,
657686 'permissions ' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE ,
658687 'size ' => (int )($ object ['Size ' ] ?? $ object ['ContentLength ' ]),
659688 ];
@@ -666,7 +695,7 @@ private function getDirectoryMetaData(string $path): ?array {
666695 if ($ this ->versioningEnabled () && !$ this ->doesDirectoryExist ($ path )) {
667696 return null ;
668697 }
669- $ cacheEntry = $ this ->getCache ()->get ($ path );
698+ $ cacheEntry = $ this ->getCache ()->get ($ this -> getCachePath ( $ path) );
670699 if ($ cacheEntry instanceof CacheEntry) {
671700 return $ cacheEntry ->getData ();
672701 } else {
0 commit comments