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