From 0d00b17d9dc6dbc0aca53ab22f77aa29cb7ccd8c Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 14 Mar 2026 12:37:26 -0400 Subject: [PATCH 1/9] docs(View): update docblock and comments in getFileInfo Signed-off-by: Josh --- lib/private/Files/View.php | 43 +++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 623c9d66d6e2a..62f39674713f3 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1423,53 +1423,79 @@ private function getCacheEntry($storage, $internalPath, $relativePath) { } /** - * get the filesystem info + * Get filesystem metadata for a path within this view. * - * @param string $path - * @param bool|string $includeMountPoints true to add mountpoint sizes, - * 'ext' to add only ext storage mount point sizes. Defaults to true. - * @return FileInfo|false False if file does not exist - */ - public function getFileInfo($path, $includeMountPoints = true) { + * @param string $path Path relative to the current view root. + * @param bool|'ext' $includeMountPoints + * true to include sub-mount sizes for directories, + * 'ext' to include only external-storage sub-mount sizes, + * false to skip sub-mount size aggregation. + * @return FileInfo|false + * Returns FileInfo when metadata is available. + * Returns false if the path is invalid, cannot be resolved from cache/storage, + * or does not exist. + * NOTE: For partial-upload paths, metadata is resolved via + * the partial-file fallback path. + */ + public function getFileInfo(string $path, bool|string $includeMountPoints = true): FileInfo|false { + // Enforce path length limits early (security / DB constraints / backend constraints) $this->assertPathLength($path); + + // Reject invalid user-provided paths before normalization/mount resolution if (!Filesystem::isValidPath($path)) { return false; } + + // Keep the original (caller-relative) path for cache lookup and partial-file handling $relativePath = $path; + + // Build normalized absolute path inside the view's fake root $path = Filesystem::normalizePath($this->fakeRoot . '/' . $path); + // Resolve the mount/storage responsible for this path $mount = Filesystem::getMountManager()->find($path); $storage = $mount->getStorage(); $internalPath = $mount->getInternalPath($path); + if ($storage) { + // Fetch cache metadata for this storage/path $data = $this->getCacheEntry($storage, $internalPath, $relativePath); + // If no cache entry: allow partial-upload files to resolve via specialized handling if (!$data instanceof ICacheEntry) { if (Scanner::isPartialFile($relativePath)) { return $this->getPartFileInfo($relativePath); } + // Not found in cache and not a known partial file => treat as non-existent return false; } + // Mount root of a moveable mount should be deletable by design if ($mount instanceof MoveableMount && $internalPath === '') { $data['permissions'] |= Constants::PERMISSION_DELETE; } + + // For mount root entries, overwrite cached name with current basename of resolved path if ($internalPath === '' && $data['name']) { $data['name'] = basename($path); } + // Resolve owner object (may be null/false for token-based access without FS owner context) $ownerId = $storage->getOwner($internalPath); $owner = null; if ($ownerId !== false) { // ownerId might be null if files are accessed with an access token without file system access $owner = $this->getUserObjectForOwner($ownerId); } + + // Build final FileInfo object from resolved storage/cache/mount data $info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner); + // Optionally include sizes from sub-mounts when this entry is a directory if (isset($data['fileid'])) { if ($includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') { - //add the sizes of other mount points to the folder + // add the sizes of other mount points to the folder $extOnly = ($includeMountPoints === 'ext'); $this->addSubMounts($info, $extOnly); } @@ -1477,6 +1503,7 @@ public function getFileInfo($path, $includeMountPoints = true) { return $info; } else { + // Mount exists but storage is unavailable/invalid $this->logger->warning('Storage not valid for mountpoint: ' . $mount->getMountPoint(), ['app' => 'core']); } From e9b0bf0eb8e9011770d23460fdc360cea27f6be9 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 14 Mar 2026 12:51:51 -0400 Subject: [PATCH 2/9] refactor(View): streamline getFileInfo to use early returns Signed-off-by: Josh --- lib/private/Files/View.php | 84 ++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 62f39674713f3..0c87c084c8314 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1438,7 +1438,7 @@ private function getCacheEntry($storage, $internalPath, $relativePath) { * the partial-file fallback path. */ public function getFileInfo(string $path, bool|string $includeMountPoints = true): FileInfo|false { - // Enforce path length limits early (security / DB constraints / backend constraints) + // Enforce path length limits early (security / backend constraints) $this->assertPathLength($path); // Reject invalid user-provided paths before normalization/mount resolution @@ -1452,62 +1452,58 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true // Build normalized absolute path inside the view's fake root $path = Filesystem::normalizePath($this->fakeRoot . '/' . $path); - // Resolve the mount/storage responsible for this path + // Resolve mount + storage context for the path $mount = Filesystem::getMountManager()->find($path); $storage = $mount->getStorage(); $internalPath = $mount->getInternalPath($path); - if ($storage) { - // Fetch cache metadata for this storage/path - $data = $this->getCacheEntry($storage, $internalPath, $relativePath); - - // If no cache entry: allow partial-upload files to resolve via specialized handling - if (!$data instanceof ICacheEntry) { - if (Scanner::isPartialFile($relativePath)) { - return $this->getPartFileInfo($relativePath); - } - - // Not found in cache and not a known partial file => treat as non-existent - return false; - } + if (!$storage) { + // Mount exists but storage is unavailable/invalid + $this->logger->warning('Storage not valid for mountpoint: ' . $mount->getMountPoint(), ['app' => 'core']); + return false; + } + + // Fetch cache metadata for the resolved storage/path + $data = $this->getCacheEntry($storage, $internalPath, $relativePath); - // Mount root of a moveable mount should be deletable by design - if ($mount instanceof MoveableMount && $internalPath === '') { - $data['permissions'] |= Constants::PERMISSION_DELETE; + // Allow partial-upload files to resolve via specialized handling; otherwise path is considered missing + if (!$data instanceof ICacheEntry) { + if (Scanner::isPartialFile($relativePath)) { + return $this->getPartFileInfo($relativePath); } + // Not found in cache and not a known partial file => treat as non-existent + return false; + } - // For mount root entries, overwrite cached name with current basename of resolved path - if ($internalPath === '' && $data['name']) { - $data['name'] = basename($path); - } + // Ensure delete permission on moveable mount roots + if ($mount instanceof MoveableMount && $internalPath === '') { + $data['permissions'] |= Constants::PERMISSION_DELETE; + } - // Resolve owner object (may be null/false for token-based access without FS owner context) - $ownerId = $storage->getOwner($internalPath); - $owner = null; - if ($ownerId !== false) { - // ownerId might be null if files are accessed with an access token without file system access - $owner = $this->getUserObjectForOwner($ownerId); - } + // Ensure mount root entries use current basename rather than stale cache name + if ($internalPath === '' && $data['name']) { + $data['name'] = basename($path); + } - // Build final FileInfo object from resolved storage/cache/mount data - $info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner); + // Resolve owner object (may remain null for token-only access contexts) + $ownerId = $storage->getOwner($internalPath); + $owner = null; + if ($ownerId !== false) { + // ownerId might be null if files are accessed with an access token without file system access + $owner = $this->getUserObjectForOwner($ownerId); + } - // Optionally include sizes from sub-mounts when this entry is a directory - if (isset($data['fileid'])) { - if ($includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') { - // add the sizes of other mount points to the folder - $extOnly = ($includeMountPoints === 'ext'); - $this->addSubMounts($info, $extOnly); - } - } + // Build final FileInfo object from resolved storage/cache/mount data + $info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner); - return $info; - } else { - // Mount exists but storage is unavailable/invalid - $this->logger->warning('Storage not valid for mountpoint: ' . $mount->getMountPoint(), ['app' => 'core']); + // Optionally include sizes from sub-mounts when this entry is a directory + if (isset($data['fileid']) && $includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') { + // add the sizes of other mount points to the folder + $extOnly = ($includeMountPoints === 'ext'); + $this->addSubMounts($info, $extOnly); } - return false; + return $info; } /** From 7978ed0cfc993424f7ed7a196828aa7addafabec Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 15 Mar 2026 00:32:03 -0400 Subject: [PATCH 3/9] refactor(View): mount/storage null-safety + related logging in getFileInfo Also use explicit path variant variable names rather than mutating and overusing vague $path var. Signed-off-by: Josh --- lib/private/Files/View.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 0c87c084c8314..6569f44e4de5d 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1446,23 +1446,25 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true return false; } - // Keep the original (caller-relative) path for cache lookup and partial-file handling + // Use the original (caller-relative) path for cache lookup and partial-file handling $relativePath = $path; - // Build normalized absolute path inside the view's fake root - $path = Filesystem::normalizePath($this->fakeRoot . '/' . $path); + $fullPath = Filesystem::normalizePath($this->fakeRoot . '/' . $relativePath); // Resolve mount + storage context for the path - $mount = Filesystem::getMountManager()->find($path); + $mount = Filesystem::getMountManager()->find($fullPath); + if (!$mount) { + $this->logger->warning('No mount found for path', ['app' => 'core', 'path' => $fullPath]); + return false; + } $storage = $mount->getStorage(); - $internalPath = $mount->getInternalPath($path); - if (!$storage) { // Mount exists but storage is unavailable/invalid - $this->logger->warning('Storage not valid for mountpoint: ' . $mount->getMountPoint(), ['app' => 'core']); + $this->logger->warning('Storage not valid for mountpoint', ['mountpoint' => $mount->getMountPoint(), 'path' => $fullPath, 'app' => 'core']); return false; } - + $internalPath = $mount->getInternalPath($fullPath); + // Fetch cache metadata for the resolved storage/path $data = $this->getCacheEntry($storage, $internalPath, $relativePath); @@ -1498,7 +1500,7 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true // Optionally include sizes from sub-mounts when this entry is a directory if (isset($data['fileid']) && $includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') { - // add the sizes of other mount points to the folder + // add the sizes of other mount points to the folder (expensive?) $extOnly = ($includeMountPoints === 'ext'); $this->addSubMounts($info, $extOnly); } From 3a45fa1c63d5c5fa35f09a2fef0d6ea5974951e1 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 15 Mar 2026 00:55:19 -0400 Subject: [PATCH 4/9] fix(View): handle valid but "truthy" names robustly in getFileInfo Signed-off-by: Josh --- lib/private/Files/View.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 6569f44e4de5d..e65f273d5d873 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1483,8 +1483,8 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true } // Ensure mount root entries use current basename rather than stale cache name - if ($internalPath === '' && $data['name']) { - $data['name'] = basename($path); + if ($internalPath === '' && $data['name'] !== '' && $data['name'] !== null) { + $data['name'] = basename($fullPath); } // Resolve owner object (may remain null for token-only access contexts) @@ -1496,7 +1496,7 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true } // Build final FileInfo object from resolved storage/cache/mount data - $info = new FileInfo($path, $storage, $internalPath, $data, $mount, $owner); + $info = new FileInfo($fullPath, $storage, $internalPath, $data, $mount, $owner); // Optionally include sizes from sub-mounts when this entry is a directory if (isset($data['fileid']) && $includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') { From 150e13059e9d25ca0b65d617622034390ac6f71a Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 15 Mar 2026 01:07:11 -0400 Subject: [PATCH 5/9] refactor(View): make partial file handling in getFileInfo more explicit Signed-off-by: Josh --- lib/private/Files/View.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index e65f273d5d873..021d31e6eb48f 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1467,14 +1467,13 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true // Fetch cache metadata for the resolved storage/path $data = $this->getCacheEntry($storage, $internalPath, $relativePath); - - // Allow partial-upload files to resolve via specialized handling; otherwise path is considered missing - if (!$data instanceof ICacheEntry) { - if (Scanner::isPartialFile($relativePath)) { - return $this->getPartFileInfo($relativePath); + if ($data === false) { + if (!Scanner::isPartialFile($relativePath)) { + // Not found in cache and not a known partial file => truly non-existent + return false; } - // Not found in cache and not a known partial file => treat as non-existent - return false; + // Partial (upload) files get a second chance + return $this->getPartFileInfo($relativePath); } // Ensure delete permission on moveable mount roots From 2d993ee09128fa40601dd0c22fdb5270bdc15257 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 15 Mar 2026 09:11:59 -0400 Subject: [PATCH 6/9] refactor(View): correct outdated getOwner context in getFileInfo Signed-off-by: Josh --- lib/private/Files/View.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 021d31e6eb48f..dd1bea6c2a7d5 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1486,13 +1486,11 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true $data['name'] = basename($fullPath); } - // Resolve owner object (may remain null for token-only access contexts) + // Resolve owner object if storage can provide an owner id $ownerId = $storage->getOwner($internalPath); - $owner = null; - if ($ownerId !== false) { - // ownerId might be null if files are accessed with an access token without file system access - $owner = $this->getUserObjectForOwner($ownerId); - } + $owner = ($ownerId !== false) + ? $this->getUserObjectForOwner($ownerId) + : null; // Build final FileInfo object from resolved storage/cache/mount data $info = new FileInfo($fullPath, $storage, $internalPath, $data, $mount, $owner); From 57138214e207427f69391bd3a48ce461ec20ac6a Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 15 Mar 2026 09:49:51 -0400 Subject: [PATCH 7/9] refactor(View): improve data/ICacheEntry mutation logic in getFileInfo Signed-off-by: Josh --- lib/private/Files/View.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index dd1bea6c2a7d5..c5df5f5f9cdb1 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1476,13 +1476,13 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true return $this->getPartFileInfo($relativePath); } - // Ensure delete permission on moveable mount roots - if ($mount instanceof MoveableMount && $internalPath === '') { + // Ensure moveable mount roots get delete permission + if ($internalPath === '' && $mount instanceof MoveableMount) { $data['permissions'] |= Constants::PERMISSION_DELETE; } - // Ensure mount root entries use current basename rather than stale cache name - if ($internalPath === '' && $data['name'] !== '' && $data['name'] !== null) { + // Ensure mount roots use current basename rather than stale cache name + if ($internalPath === '' && ($data['name'] ?? '') !== '') { $data['name'] = basename($fullPath); } From a701c003ad0a80237e797846bad09bee9fa5f67d Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 15 Mar 2026 10:02:09 -0400 Subject: [PATCH 8/9] docs(View): owner lazy load possibility Signed-off-by: Josh --- lib/private/Files/View.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index c5df5f5f9cdb1..58bc7f7b9fe72 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1487,6 +1487,7 @@ public function getFileInfo(string $path, bool|string $includeMountPoints = true } // Resolve owner object if storage can provide an owner id + // @todo: lazy load owner resolution in FileInfo instead? $ownerId = $storage->getOwner($internalPath); $owner = ($ownerId !== false) ? $this->getUserObjectForOwner($ownerId) From 6bc9c3c2ddb5d96f4c3eb7df0ac6e5ac327a9e4e Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 15 Mar 2026 10:18:37 -0400 Subject: [PATCH 9/9] chore(View): drop unnecessary comments from getFileInfo refctor Signed-off-by: Josh --- lib/private/Files/View.php | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 58bc7f7b9fe72..e5fbe2d6836b9 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1438,69 +1438,59 @@ private function getCacheEntry($storage, $internalPath, $relativePath) { * the partial-file fallback path. */ public function getFileInfo(string $path, bool|string $includeMountPoints = true): FileInfo|false { - // Enforce path length limits early (security / backend constraints) $this->assertPathLength($path); - // Reject invalid user-provided paths before normalization/mount resolution if (!Filesystem::isValidPath($path)) { return false; } - // Use the original (caller-relative) path for cache lookup and partial-file handling $relativePath = $path; - // Build normalized absolute path inside the view's fake root $fullPath = Filesystem::normalizePath($this->fakeRoot . '/' . $relativePath); - // Resolve mount + storage context for the path $mount = Filesystem::getMountManager()->find($fullPath); if (!$mount) { $this->logger->warning('No mount found for path', ['app' => 'core', 'path' => $fullPath]); return false; } + $storage = $mount->getStorage(); if (!$storage) { - // Mount exists but storage is unavailable/invalid - $this->logger->warning('Storage not valid for mountpoint', ['mountpoint' => $mount->getMountPoint(), 'path' => $fullPath, 'app' => 'core']); + $this->logger->warning('Storage not valid for mountpoint', ['app' => 'core', 'mountpoint' => $mount->getMountPoint(), 'path' => $fullPath]); return false; } - $internalPath = $mount->getInternalPath($fullPath); - // Fetch cache metadata for the resolved storage/path + $internalPath = $mount->getInternalPath($fullPath); $data = $this->getCacheEntry($storage, $internalPath, $relativePath); - if ($data === false) { + + if ($data === false) { // cache miss if (!Scanner::isPartialFile($relativePath)) { - // Not found in cache and not a known partial file => truly non-existent return false; } - // Partial (upload) files get a second chance + // Partial uploads can exist without a regular cache entry. return $this->getPartFileInfo($relativePath); } - // Ensure moveable mount roots get delete permission + // Moveable mount roots must remain deletable. if ($internalPath === '' && $mount instanceof MoveableMount) { $data['permissions'] |= Constants::PERMISSION_DELETE; } - // Ensure mount roots use current basename rather than stale cache name + // Root cache names can be stale; derive from the resolved full path. if ($internalPath === '' && ($data['name'] ?? '') !== '') { $data['name'] = basename($fullPath); } - // Resolve owner object if storage can provide an owner id // @todo: lazy load owner resolution in FileInfo instead? $ownerId = $storage->getOwner($internalPath); $owner = ($ownerId !== false) ? $this->getUserObjectForOwner($ownerId) : null; - // Build final FileInfo object from resolved storage/cache/mount data $info = new FileInfo($fullPath, $storage, $internalPath, $data, $mount, $owner); - // Optionally include sizes from sub-mounts when this entry is a directory - if (isset($data['fileid']) && $includeMountPoints && $data['mimetype'] === 'httpd/unix-directory') { - // add the sizes of other mount points to the folder (expensive?) + if ($includeMountPoints && $data['mimetype'] === 'httpd/unix-directory' && isset($data['fileid'])) { $extOnly = ($includeMountPoints === 'ext'); - $this->addSubMounts($info, $extOnly); + $this->addSubMounts($info, $extOnly); // expensive? } return $info;