Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Team folders always appear as special shared folders in users’ file lists, reg

All access control, configuration, and permissions for Team folders are managed within the Nextcloud database and UI as before.


## Setting Advanced Permissions

_Advanced Permissions_ allows entitled users to configure permissions inside Team folders on a per file and folder basis.
Expand Down
23 changes: 16 additions & 7 deletions lib/TeamSpace/TeamSpaceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,22 @@ public function removeTeamFolder(string $teamId): bool {
* @return list<TeamResource>
*/
public function getSharedWith(string $teamId): array {
return array_map(fn (TeamFolder $folder): TeamResource => new TeamResource(
$this,
(string)$folder->getId(),
$folder->getMountPoint(),
$this->urlGenerator->getAbsoluteURL('/apps/files/?dir=/' . rawurlencode($folder->getMountPoint())),
iconSvg: $this->getIconSvg(),
), $this->service->getGroupFoldersForCircle($teamId));
$teamSpaceFolder = $this->service->getTeamSpaceForCircle($teamId);
$teamSpaceFolderId = $teamSpaceFolder !== null ? (string)$teamSpaceFolder->getId() : null;

return array_map(function (TeamFolder $folder) use ($teamId, $teamSpaceFolderId): TeamResource {
$url = $teamSpaceFolderId !== null && (string)$folder->getId() === $teamSpaceFolderId
? $this->urlGenerator->linkToRouteAbsolute('circles.page.indexpath', ['path' => 'team/' . $teamId])
: $this->urlGenerator->linkToRouteAbsolute('files.view.index', ['dir' => '/' . $folder->getMountPoint()]);

return new TeamResource(
$this,
(string)$folder->getId(),
$folder->getMountPoint(),
$url,
iconSvg: $this->getIconSvg(),
);
}, $this->service->getGroupFoldersForCircle($teamId));
}

#[\Override]
Expand Down
36 changes: 36 additions & 0 deletions lib/TeamSpace/TeamSpaceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use OCA\GroupFolders\Folder\FolderDefinition;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Mount\FolderStorageManager;
use OCP\Teams\Team;
use OCP\Teams\TeamFolder;
use Psr\Log\LoggerInterface;
Expand All @@ -31,8 +32,11 @@
* needed.
*/
class TeamSpaceService {
private const string APP_DIRECTORY_NAME = '.system';

public function __construct(
private readonly FolderManager $folderManager,
private readonly FolderStorageManager $folderStorageManager,
private readonly LoggerInterface $logger,
) {
}
Expand All @@ -51,6 +55,8 @@ public function createTeamSpace(string $circleId, string $mountPoint, int $quota
$folderId = $this->folderManager->createFolder($mountPoint);

try {
$this->createAppDirectory($folderId);

if ($quota > 0) {
$this->folderManager->setFolderQuota($folderId, $quota);
}
Expand Down Expand Up @@ -98,6 +104,34 @@ public function createTeamSpace(string $circleId, string $mountPoint, int $quota
return $folderId;
}

/**
* Create the folder reserved for app data in a team space.
*
* @throws \RuntimeException when the folder cannot be created.
*/
private function createAppDirectory(int $folderId): void {
$teamSpace = $this->folderManager->getFolder($folderId);
if ($teamSpace === null) {
throw new \RuntimeException('Created team space could not be found');
}

$storage = $this->folderStorageManager->getBaseStorageForFolder(
$folderId,
$teamSpace->useSeparateStorage(),
$teamSpace,
);

if ((bool)$storage->is_dir(self::APP_DIRECTORY_NAME)) {
return;
}

if (!$storage->mkdir(self::APP_DIRECTORY_NAME)) {
throw new \RuntimeException('Could not create ' . self::APP_DIRECTORY_NAME . ' folder for team space');
}

$storage->getScanner()->scan(self::APP_DIRECTORY_NAME);
}

/**
* Unlink the team space from a team without deleting the folder.
*
Expand Down Expand Up @@ -197,6 +231,8 @@ public function getTeamSpaceForCircle(string $circleId): ?TeamFolder {
return null;
}

$this->createAppDirectory($folderId);

return new TeamFolder($folder->id, $folder->mountPoint);
}

Expand Down
13 changes: 10 additions & 3 deletions tests/TeamSpace/TeamSpaceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ protected function setUp(): void {
}

public function testGetSharedWithReturnsAllFoldersAssignedToTeam(): void {
$this->service->expects($this->once())
->method('getTeamSpaceForCircle')
->with('team-1')
->willReturn(new TeamFolder(42, 'Engineering'));
$this->service->expects($this->once())
->method('getGroupFoldersForCircle')
->with('team-1')
Expand All @@ -44,15 +48,18 @@ public function testGetSharedWithReturnsAllFoldersAssignedToTeam(): void {
new TeamFolder(43, 'Shared projects'),
]);
$this->urlGenerator->expects($this->exactly(2))
->method('getAbsoluteURL')
->willReturnCallback(static fn (string $url): string => 'https://cloud.example' . $url);
->method('linkToRouteAbsolute')
->willReturnMap([
['circles.page.indexpath', ['path' => 'team/team-1'], 'https://cloud.example/index.php/apps/circles/teams/team/team-1'],
['files.view.index', ['dir' => '/Shared projects'], 'https://cloud.example/apps/files/?dir=/Shared%20projects'],
]);

$resources = $this->provider->getSharedWith('team-1');

$this->assertCount(2, $resources);
$this->assertSame('42', $resources[0]->getId());
$this->assertSame('Engineering', $resources[0]->getLabel());
$this->assertSame('https://cloud.example/apps/files/?dir=/Engineering', $resources[0]->getUrl());
$this->assertSame('https://cloud.example/index.php/apps/circles/teams/team/team-1', $resources[0]->getUrl());
$this->assertSame('43', $resources[1]->getId());
$this->assertSame('Shared projects', $resources[1]->getLabel());
$this->assertSame('https://cloud.example/apps/files/?dir=/Shared%20projects', $resources[1]->getUrl());
Expand Down
89 changes: 89 additions & 0 deletions tests/TeamSpace/TeamSpaceServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,119 @@

use OCA\GroupFolders\Folder\FolderDefinition;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Folder\FolderWithMappingsAndCache;
use OCA\GroupFolders\Mount\FolderStorageManager;
use OCA\GroupFolders\TeamSpace\TeamSpaceService;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Cache\IScanner;
use OCP\Files\Storage\IStorage;
use OCP\Teams\Team;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;

class TeamSpaceServiceTest extends TestCase {
private FolderManager&MockObject $folderManager;
private FolderStorageManager&MockObject $folderStorageManager;
private TeamSpaceService $service;

#[\Override]
protected function setUp(): void {
parent::setUp();
$this->folderManager = $this->createMock(FolderManager::class);
$this->folderStorageManager = $this->createMock(FolderStorageManager::class);
$this->service = new TeamSpaceService(
$this->folderManager,
$this->folderStorageManager,
$this->createMock(LoggerInterface::class),
);
}

public function testCreateTeamSpaceStoresOnlyTeamSpaceLink(): void {
$teamSpace = $this->createTeamSpaceFolder();
$storage = $this->createMock(IStorage::class);
$scanner = $this->createMock(IScanner::class);
$this->folderManager->method('createFolder')->with('Engineering')->willReturn(42);
$this->folderManager->method('getFolder')->with(42)->willReturn($teamSpace);
$this->folderStorageManager->expects($this->once())->method('getBaseStorageForFolder')->with(42, false, $teamSpace)->willReturn($storage);
$storage->expects($this->once())->method('is_dir')->with('.system')->willReturn(false);
$storage->expects($this->once())->method('mkdir')->with('.system')->willReturn(true);
$storage->expects($this->once())->method('getScanner')->willReturn($scanner);
$scannedPaths = [];
$scanner->expects($this->once())->method('scan')->willReturnCallback(
static function (string $path) use (&$scannedPaths): void {
$scannedPaths[] = $path;
},
);
$this->folderManager->expects($this->once())->method('setFolderQuota')->with(42, 1024);
$this->folderManager->expects($this->once())->method('addApplicableGroup')->with(42, 'team-1');
$this->folderManager->expects($this->once())->method('setManageACL')->with(42, 'circle', 'team-1', true);
$this->folderManager->expects($this->once())->method('setTeamCircleId')->with(42, 'team-1');

$this->assertSame(42, $this->service->createTeamSpace('team-1', 'Engineering', 1024));
$this->assertSame(['.system'], $scannedPaths);
}

public function testCreateTeamSpaceDoesNotRecreateExistingAppDirectory(): void {
$teamSpace = $this->createTeamSpaceFolder();
$storage = $this->createMock(IStorage::class);
$this->folderManager->method('createFolder')->with('Engineering')->willReturn(42);
$this->folderManager->method('getFolder')->with(42)->willReturn($teamSpace);
$this->folderStorageManager->method('getBaseStorageForFolder')->with(42, false, $teamSpace)->willReturn($storage);
$storage->expects($this->once())->method('is_dir')->with('.system')->willReturn(true);
$storage->expects($this->never())->method('mkdir');
$storage->expects($this->never())->method('getScanner');

$this->service->createTeamSpace('team-1', 'Engineering');
}

public function testCreateTeamSpaceRollsBackWhenAppDirectoryCannotBeCreated(): void {
$teamSpace = $this->createTeamSpaceFolder();
$storage = $this->createMock(IStorage::class);
$this->folderManager->method('createFolder')->with('Engineering')->willReturn(42);
$this->folderManager->method('getFolder')->with(42)->willReturn($teamSpace);
$this->folderManager->method('getFolderIdByTeamCircleId')->with('team-1')->willReturn(null);
$this->folderStorageManager->method('getBaseStorageForFolder')->with(42, false, $teamSpace)->willReturn($storage);
$storage->method('is_dir')->with('.system')->willReturn(false);
$storage->method('mkdir')->with('.system')->willReturn(false);
$this->folderManager->expects($this->once())->method('clearTeamCircleId')->with(42);
$this->folderManager->expects($this->once())->method('removeFolder')->with(42);

$this->expectException(\RuntimeException::class);
$this->service->createTeamSpace('team-1', 'Engineering');
}

public function testUpgradeUsesPublicTeamValue(): void {
$team = new Team('team-1', 'Engineering', null);
$teamSpace = $this->createTeamSpaceFolder();
$storage = $this->createMock(IStorage::class);
$scanner = $this->createMock(IScanner::class);
$this->folderManager->method('getFolderIdByTeamCircleId')->with('team-1')->willReturn(null);
$this->folderManager->method('mountPointExists')->willReturn(false);
$this->folderManager->expects($this->once())->method('createFolder')->with('Engineering')->willReturn(42);
$this->folderManager->method('getFolder')->with(42)->willReturn($teamSpace);
$this->folderStorageManager->method('getBaseStorageForFolder')->with(42, false, $teamSpace)->willReturn($storage);
$storage->method('is_dir')->with('.system')->willReturn(false);
$storage->method('mkdir')->with('.system')->willReturn(true);
$storage->method('getScanner')->willReturn($scanner);
$this->folderManager->expects($this->once())->method('setTeamCircleId')->with(42, 'team-1');

$this->assertSame(42, $this->service->upgradeTeamSpace($team));
}

public function testGetTeamSpaceForCircleDoesNotRecreateExistingAppDirectory(): void {
$teamSpace = $this->createTeamSpaceFolder();
$storage = $this->createMock(IStorage::class);
$this->folderManager->method('getFolderIdByTeamCircleId')->with('team-1')->willReturn(42);
$this->folderManager->method('getFolder')->with(42)->willReturn($teamSpace);
$this->folderStorageManager->expects($this->once())->method('getBaseStorageForFolder')->with(42, false, $teamSpace)->willReturn($storage);
$storage->expects($this->once())->method('is_dir')->with('.system')->willReturn(true);
$storage->expects($this->never())->method('mkdir');
$storage->expects($this->never())->method('getScanner');

$this->assertSame(42, $this->service->getTeamSpaceForCircle('team-1')?->getId());
}

public function testUnlinkKeepsFolderAndClearsTeamLink(): void {
$this->folderManager->method('getFolderIdByTeamCircleId')->with('team-1')->willReturn(42);
$this->folderManager->expects($this->once())->method('clearTeamCircleId')->with(42);
Expand Down Expand Up @@ -84,4 +157,20 @@ public function testPickBaseNameUsesDisplayName(): void {
public function testSanitizeMountPointStripsControlCharsAndSeparators(): void {
$this->assertSame('Engineering', $this->service->sanitizeMountPoint("Engi\nnee/rin\\g"));
}

private function createTeamSpaceFolder(): FolderWithMappingsAndCache {
return new FolderWithMappingsAndCache(
42,
'Engineering',
0,
false,
false,
1,
99,
[],
[],
[],
$this->createMock(ICacheEntry::class),
);
}
}
Loading