Repository navigation
feat: allow mounting team folders below a configurable parent folder - #5205
valueerrorx wants to merge 9 commits into
Conversation
Admins can now configure a system-wide parent folder (e.g. "TeamFolders") in which all team folders are mounted for every user, instead of cluttering the root of the users' files. - New MountPathResolver translates between the stored mount point and the per-user mount path; configured via app config `groupfolders mount_prefix`. Empty (default) keeps the current behaviour. - MountProvider applies the prefix for full and partial setups and creates the parent folder(s) in the user's home, renaming a file that would block the path. - Trash (original location / restore) and the groupfolders DAV home use the prefixed path. - New admin settings section to change the parent folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Thomas Weissel <valueerror@gmail.com>
The folder list has a min-height of the full viewport, which pushed the section far down the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Thomas Weissel <valueerror@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Thomas Weissel <valueerror@gmail.com>
07a5ec7 to
1d3ab48
Compare
|
Thanks for working on this. One consideration: the prefix applies to every group folder, including admin-created ones such as a main shared folder, which would change all existing paths (WebDAV, share targets, scripts). Would it be possible to apply the prefix only to Team-linked (auto-created) folders, or make it opt-in per folder? |
Admin created folders keep their path unless mount_prefix_scope is set to "all", so existing paths (WebDAV, desktop clients, links) don't change unexpectedly when the parent folder is configured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Thomas Weissel <valueerror@gmail.com>
The provisioning API requires a recent password confirmation, so saving failed when the admin had not confirmed their password in the last 30 minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Thomas Weissel <valueerror@gmail.com>
|
Updated: the prefix now only applies to Team-linked folders by default, admin folders keep their paths unless the scope is set to all. |
getMountsForPath() no longer mapped the root of the user's files to the "/" mount point, so a folder mounted at "/" (see FolderManager::trimMountpoint()) was not found by the partial mount setup, regardless of the parent folder setting. Such folders are also never moved below the parent folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Thomas Weissel <valueerror@gmail.com>
| foreach ($folders as $folder) { | ||
| if ($this->mountPathResolver->appliesTo($folder)) { | ||
| $this->ensurePrefixFolderExists($user); | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
Prefix directory is created in the wrong storage when a root-mounted GroupFolder exists. Consequently, the prefix directory creation has no effect on the visible root-mounted filesystem. The nested team folder is still registered, but its expected parent directory is missing from the storage underneath it.
Handle prefix-directory creation against the effective filesystem storage, including the root-mounted GroupFolder case.
Add an integration test that:
- Creates a root-mounted GroupFolder.
- Creates a team-linked GroupFolder for the same user.
- Enables a non-empty prefix.
- Performs full filesystem setup.
- Verifies that the prefix directory is actually accessible and listable and that the nested team folder works.
| /** | ||
| * Whether the parent folder is used for the given folder. | ||
| */ | ||
| public function appliesTo(FolderDefinition $folder): bool { | ||
| // A folder mounted at "/" replaces the user's home folder and is never moved | ||
| if ($this->getPrefix() === '' || $folder->mountPoint === self::ROOT_MOUNT_POINT) { | ||
| return false; | ||
| } | ||
|
|
||
| return $this->getScope() === self::SCOPE_ALL || $folder->isTeamSpace(); | ||
| } | ||
|
|
||
| /** | ||
| * Path relative to the user's "files" folder where the given folder is mounted. | ||
| */ | ||
| public function getUserMountPath(FolderDefinition $folder): string { | ||
| if (!$this->appliesTo($folder)) { | ||
| return $folder->mountPoint; | ||
| } | ||
|
|
||
| return $this->getPrefix() . '/' . $folder->mountPoint; | ||
| } |
There was a problem hiding this comment.
Effective mount-point collisions are not detected. The resolver determines paths independently for each folder, but does not guarantee that the resulting paths are unique.
Validate effective mount paths per user before returning mounts.
At minimum:
- Detect duplicate resolved paths.
- Do not silently overwrite a mount.
- Define deterministic handling for collisions between prefixed team folders and existing nested admin folders.
- Ensure that TeamResource URLs use the effective user-facing mount path rather than the stored database mount point.
- Add tests covering both full filesystem setup and partial setup.
There was a problem hiding this comment.
Thanks, fixed in 3079df9.
- Collisions: paths are now resolved per user with one deterministic rule, used by both the full and the partial setup. Folders that are not moved keep their stored mount point. A team folder whose path below the parent folder is taken falls back to its stored mount point. If no path is free, the folder is skipped with a warning; the lower id wins. Nothing is overwritten silently.
- Prevention:
mountPointExists()also checks the resulting paths, so colliding names are rejected and new team folders get a unique name. - TeamResource:
TeamSpaceProviderand thegetSharedWith()links return the effective path of the current user. - Tests:
MountProviderTestchecks that the full and the partial setup give the same result across several scenarios (root mount, collisions, nested admin folders, duplicates) and prefix configs.
Collision rules are documented in the README.
The parent folder is added at mount time, so two folders can end up at the same path for a user, e.g. the team folder "Project" and an admin folder stored as "TeamFolders/Project". Such mounts silently replaced each other. - MountPathResolver::resolveMountPaths() resolves the paths of all folders of a user deterministically: folders that are not moved keep their stored mount point, a moved folder whose path is taken falls back to its stored mount point, and a folder without a free path is not mounted. Fallbacks and skipped folders are logged. - The full and the partial filesystem setup use the same resolution, the partial setup also looks up the folders a requested folder can collide with, so both mount every folder at the same path. - The parent folder is not created in the home storage when a group folder is mounted at (a part of) it or at "/". - FolderManager::mountPointExists() also detects collisions of the resulting paths, so new admin and team folders cannot create them. - The Teams integration reports the path of the current user as the team folder mount point and uses it for the Files links, the Teams app uses it as path inside the user's files. - Trash and the groupfolders WebDAV home use the resolved paths. - Document the parent folder and the collision handling in the README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Thomas Weissel <valueerror@gmail.com>
|
Thanks, fixed in 3079df9.
Collision rules are documented in the README. |

Summary
Since team folders can be created by any team owner (Teams → "create team folder"), the root of every user's files quickly fills up with team folders. This PR adds an optional, system-wide parent folder in which team folders are mounted for every user, e.g.
TeamFolders/Marketinginstead ofMarketing.occ config:app:set groupfolders mount_prefix --value="TeamFolders"Shared/Teamsare supported.team_circle_idis set) are moved. Admin created folders keep their path, so existing WebDAV paths, desktop client sync paths and links don't change. A switch in the settings (mount_prefix_scope=all) applies the parent folder to all folders./(replacing the user's home folder) are never moved.Mount path collisions
Because the prefix is applied at mount time, two folders can end up at the same path for a user (e.g. the team folder
Projectand an admin folder stored asTeamFolders/Project). They are resolved per user with one deterministic rule, used by both the full and the partial setup:Nothing is overwritten silently. New collisions are prevented:
FolderManager::mountPointExists()also checks the resulting paths, so the admin UI andoccreject colliding names and new team folders get a unique name (Lab (1)).If a group folder is mounted at the parent folder or at a part of it, team folders are nested inside it like any other nested group folder, and the parent folder is not created in the home storage.
Implementation
MountPathResolver(new) translates between the stored mount point and the per-user mount path, depending on the folder (isTeamSpace()) and the scope, and sanitizes the setting (..,., empty segments, backslashes).MountProvider: applies the resolved paths ingetMountsForUser()andgetMountsForPath(). For partial setupsMountPathResolver::getMountPointCandidates()loads every folder that can be mounted at or collide with the requested path (<path>,<path without prefix>,<prefix>/<path>), and only folders whose resolved path matches the requested one are returned. It also makes sure the parent folder(s) exist in the user's home, so deep paths can be reached directly without listing the parent first. A file blocking the path is renamed, like existing mount point conflicts.TeamSpaceProvider: the Teams app usesTeamFolder::getMountPoint()as the path inside the current user's files (folder widget, team pages, Files links), so it returns the resolved path of the current user. The Files links ingetSharedWith()use it as well; labels keep the stored name.TrashBackend: the original location and restore target use the resolved path.GroupFoldersHome(DAV): resolves and nests nodes via the resolved paths.AdminMountPrefix,TeamFoldersMountPrefix.vue), saved through the provisioning API like the encryption setting.Testing
MountProviderTest::testFullAndPartialSetupAgreeruns 7 scenarios (regular, root mount, team vs. admin collision, admin folder at / nested in the parent folder, fallback also taken, duplicate mount points) with no prefix, scopeteamsand scopeall, and asserts that the full setup never mounts two folders at one path and that the partial setup returns the same folder for every path. 12 of these cases fail without the collision handling.MountPathResolver,FolderManager::mountPointExists()andTeamSpaceProvider.CircleDestroyedEventListenerTest, which needs the circles app). phpstan and php-cs-fixer are clean.TeamFolders/Marketing/test.txtas original location; restore worksTeamFolders→Schule/Teams→TeamFolders→ empty, no errors in the logteams: an admin created folder stays atMarketingwhile a team folder is atTeamFolders/Projekt X; scopeall: both are below the parent foldergetMountsForPath()called directly (asSetupManagerdoes) with Redis as distributed cache, for both scopes, single and nested prefixes and an empty prefixTeamFolders/Docsare resolved the same way in the full and the partial setup,occand the admin UI reject colliding names, team pages are created in the right folder, trash restore worksKnown limitations
teams, a folder that is unlinked from its team but still shared with a group moves back to the root of the users' files.fs_mount_cache_duration, default 5 min).occmay be picked up by the web server only after its local app config cache expires.TeamFolders (1)), like any other mount point conflict.DocsandTeamFolders/Docs), thegroupfoldersWebDAV home lists both asDocs, as it already does for nested group folders.Checklist
This change was developed with AI assistance (Claude Code) and reviewed and tested by me.
🤖 Generated with Claude Code