Steps to reproduce
On an instance with separate-storage team folders and local (non-object) primary storage:
occ groupfolders:create probe
- Note the new row in
oc_storages: local::<datadir>/__groupfolders/<folder_id>/
occ groupfolders:delete <folder_id> --force
SELECT * FROM oc_storages WHERE id LIKE '%__groupfolders%';
Expected behaviour
The folder's oc_storages row is deleted with the folder, the way occ user:delete removes a
user's home:: row.
Actual behaviour
The folder is gone from oc_group_folders, its oc_filecache rows are gone, the directory under
<datadir>/__groupfolders/<id> is gone — and the oc_storages row is still there. One row is
orphaned per team folder the instance has ever deleted. On our instance twenty had accumulated;
occ files:scan --all and occ files:cleanup both report 0 and neither collects them, and
there is no occ verb and no API route that does.
Cause, traced at master (38ab1c1e)
lib/Mount/FolderStorageManager.php:272 deleteStoragesForFolder() fetches the folder's storage
through getBaseStorageForFolder() and calls $storage->getCache()->clear().
For a separate-storage folder that storage comes from
getBaseStorageForFolderSeparate(), which at :131-134 always returns the Local
(:137-162, rooted at <datadir>/__groupfolders/<id>) wrapped in a Jail:
return new Jail([
'storage' => $storage,
'root' => $type,
]);
So getCache() returns a CacheJail, whose clear() is
(server lib/private/Files/Cache/Wrapper/CacheJail.php:223):
public function clear() {
$this->getCache()->remove($this->getRoot());
}
That removes the jailed subtree from oc_filecache and nothing else. The clear() that deletes
the storage row is the unwrapped one, server lib/private/Files/Cache/Cache.php:884:
$query->delete('storages')
->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
and the delete path never reaches it, because the storage is jailed on every way out. The
/** @var Cache $cache */ annotation on :275 says the code expects the unwrapped one.
Both entry points are affected — lib/Command/Delete.php:43 and
lib/Controller/FolderController.php:304 call the same method.
Note this is not #4155 / #4157: that fix added '' to the foreach so the folder's own root
directory is rmdir'd, and it works — the directory does go. This is the other half of the same
loop, the database row.
Suggested direction
Unwrap before clearing, so the storage's own cache is the one that gets clear()ed — e.g. take
the unjailed storage once after the loop and call getCache()->clear() on it, or resolve the
numeric storage id from the wrapper and delete the oc_storages row explicitly. A repair step
for instances that already carry orphans would be welcome too; a row is safe to take when no
oc_group_folders row names it and it holds no oc_filecache, oc_mounts or oc_previews row.
Versions
Nextcloud 34.0.3 · Team folders (groupfolders) 22.0.6 · PostgreSQL · local primary storage ·
separate-storage enabled. Re-read at master 38ab1c1e and at tag v23.0.0: the function is
unchanged in both.
Steps to reproduce
On an instance with
separate-storageteam folders and local (non-object) primary storage:occ groupfolders:create probeoc_storages:local::<datadir>/__groupfolders/<folder_id>/occ groupfolders:delete <folder_id> --forceSELECT * FROM oc_storages WHERE id LIKE '%__groupfolders%';Expected behaviour
The folder's
oc_storagesrow is deleted with the folder, the wayocc user:deleteremoves auser's
home::row.Actual behaviour
The folder is gone from
oc_group_folders, itsoc_filecacherows are gone, the directory under<datadir>/__groupfolders/<id>is gone — and theoc_storagesrow is still there. One row isorphaned per team folder the instance has ever deleted. On our instance twenty had accumulated;
occ files:scan --allandocc files:cleanupboth report0and neither collects them, andthere is no
occverb and no API route that does.Cause, traced at
master(38ab1c1e)lib/Mount/FolderStorageManager.php:272deleteStoragesForFolder()fetches the folder's storagethrough
getBaseStorageForFolder()and calls$storage->getCache()->clear().For a separate-storage folder that storage comes from
getBaseStorageForFolderSeparate(), which at:131-134always returns theLocal(
:137-162, rooted at<datadir>/__groupfolders/<id>) wrapped in aJail:So
getCache()returns aCacheJail, whoseclear()is(
serverlib/private/Files/Cache/Wrapper/CacheJail.php:223):That removes the jailed subtree from
oc_filecacheand nothing else. Theclear()that deletesthe storage row is the unwrapped one,
serverlib/private/Files/Cache/Cache.php:884:and the delete path never reaches it, because the storage is jailed on every way out. The
/** @var Cache $cache */annotation on:275says the code expects the unwrapped one.Both entry points are affected —
lib/Command/Delete.php:43andlib/Controller/FolderController.php:304call the same method.Note this is not #4155 / #4157: that fix added
''to theforeachso the folder's own rootdirectory is
rmdir'd, and it works — the directory does go. This is the other half of the sameloop, the database row.
Suggested direction
Unwrap before clearing, so the storage's own cache is the one that gets
clear()ed — e.g. takethe unjailed storage once after the loop and call
getCache()->clear()on it, or resolve thenumeric storage id from the wrapper and delete the
oc_storagesrow explicitly. Arepairstepfor instances that already carry orphans would be welcome too; a row is safe to take when no
oc_group_foldersrow names it and it holds nooc_filecache,oc_mountsoroc_previewsrow.Versions
Nextcloud 34.0.3 · Team folders (groupfolders) 22.0.6 · PostgreSQL · local primary storage ·
separate-storageenabled. Re-read atmaster38ab1c1eand at tagv23.0.0: the function isunchanged in both.