From 4b732e7e1bff0c0b4bf8a14de3e1080d7088afc6 Mon Sep 17 00:00:00 2001 From: Mehdi Zakaria Benadel Date: Sat, 21 Mar 2026 18:53:46 +0100 Subject: [PATCH 1/4] Basic handling of shared recipes --- .../Implementation/ConfigImplementation.php | 22 +++ lib/Controller/MainController.php | 14 +- lib/Helper/FolderHelper.php | 142 ++++++++++++++++++ lib/Helper/MyRecipesFolderHelper.php | 37 +++++ lib/Helper/SharedRecipesFolderHelper.php | 37 +++++ lib/Helper/UserConfigHelper.php | 82 +++++++++- lib/Helper/UserFolderHelper.php | 109 +------------- lib/Service/RecipeService.php | 65 ++++++-- src/components/Modals/SettingsDialog.vue | 104 +++++++++++++ src/js/api-interface.js | 16 +- src/store/index.ts | 24 +++ 11 files changed, 530 insertions(+), 122 deletions(-) create mode 100644 lib/Helper/FolderHelper.php create mode 100644 lib/Helper/MyRecipesFolderHelper.php create mode 100644 lib/Helper/SharedRecipesFolderHelper.php diff --git a/lib/Controller/Implementation/ConfigImplementation.php b/lib/Controller/Implementation/ConfigImplementation.php index 49a0a2f63..161fdbe90 100644 --- a/lib/Controller/Implementation/ConfigImplementation.php +++ b/lib/Controller/Implementation/ConfigImplementation.php @@ -4,6 +4,8 @@ use OCA\Cookbook\Helper\RestParameterParser; use OCA\Cookbook\Helper\UserFolderHelper; +use OCA\Cookbook\Helper\MyRecipesFolderHelper; +use OCA\Cookbook\Helper\SharedRecipesFolderHelper; use OCA\Cookbook\Service\DbCacheService; use OCA\Cookbook\Service\RecipeService; use OCP\AppFramework\Http; @@ -18,17 +20,25 @@ class ConfigImplementation { private $restParser; /** @var UserFolderHelper */ private $userFolder; + /** @var MyRecipesFolderHelper */ + private $myRecipesFolder; + /** @var SharedRecipesFolderHelper */ + private $sharedRecipesFolder; public function __construct( RecipeService $recipeService, DbCacheService $dbCacheService, RestParameterParser $restParser, UserFolderHelper $userFolder, + MyRecipesFolderHelper $myRecipesFolder, + SharedRecipesFolderHelper $sharedRecipesFolder, ) { $this->service = $recipeService; $this->dbCacheService = $dbCacheService; $this->restParser = $restParser; $this->userFolder = $userFolder; + $this->myRecipesFolder = $myRecipesFolder; + $this->sharedRecipesFolder = $sharedRecipesFolder; } protected const KEY_VISIBLE_INFO_BLOCKS = 'visibleInfoBlocks'; @@ -43,6 +53,8 @@ public function list() { return new JSONResponse([ 'folder' => $this->userFolder->getPath(), + 'my_recipes_folder' => $this->myRecipesFolder->getPath(), + 'shared_recipes_folder' => $this->sharedRecipesFolder->getPath(), 'update_interval' => $this->dbCacheService->getSearchIndexUpdateInterval(), 'print_image' => $this->service->getPrintImage(), self::KEY_VISIBLE_INFO_BLOCKS => $this->service->getVisibleInfoBlocks(), @@ -67,6 +79,16 @@ public function config() { $this->dbCacheService->updateCache(); } + if (isset($data['my_recipes_folder'])) { + $this->myRecipesFolder->setPath($data['my_recipes_folder']); + $this->dbCacheService->updateCache(); + } + + if (isset($data['shared_recipes_folder'])) { + $this->sharedRecipesFolder->setPath($data['shared_recipes_folder']); + $this->dbCacheService->updateCache(); + } + if (isset($data['update_interval'])) { $this->service->setSearchIndexUpdateInterval($data['update_interval']); } diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index a2090acf6..0f873a91f 100755 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -5,6 +5,8 @@ use OCA\Cookbook\Exception\UserFolderNotWritableException; use OCA\Cookbook\Exception\UserNotLoggedInException; use OCA\Cookbook\Helper\UserFolderHelper; +use OCA\Cookbook\Helper\MyRecipesFolderHelper; +use OCA\Cookbook\Helper\SharedRecipesFolderHelper; use OCA\Cookbook\Service\DbCacheService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\NoAdminRequired; @@ -20,18 +22,26 @@ class MainController extends Controller { private $dbCacheService; /** @var UserFolderHelper */ private $userFolder; + /** @var MyRecipesFolderHelper */ + private $myRecipesFolder; + /** @var SharedRecipesFolderHelper */ + private $sharedRecipesFolder; public function __construct( string $AppName, IRequest $request, DbCacheService $dbCacheService, UserFolderHelper $userFolder, + MyRecipesFolderHelper $myRecipesFolder, + SharedRecipesFolderHelper $sharedRecipesFolder, ) { parent::__construct($AppName, $request); $this->appName = $AppName; $this->dbCacheService = $dbCacheService; $this->userFolder = $userFolder; + $this->myRecipesFolder = $myRecipesFolder; + $this->sharedRecipesFolder = $sharedRecipesFolder; } /** @@ -43,8 +53,10 @@ public function __construct( #[NoCSRFRequired] public function index(): TemplateResponse { try { - // Check if the user folder can be accessed + // Check if the user folders can be accessed $this->userFolder->getFolder(); + $this->myRecipesFolder->getFolder(); + $this->sharedRecipesFolder->getFolder(); } catch (UserFolderNotWritableException $ex) { Util::addScript('cookbook', 'cookbook-guest'); return new TemplateResponse($this->appName, 'invalid_guest'); diff --git a/lib/Helper/FolderHelper.php b/lib/Helper/FolderHelper.php new file mode 100644 index 000000000..f9f6ca347 --- /dev/null +++ b/lib/Helper/FolderHelper.php @@ -0,0 +1,142 @@ +userId = $UserId; + $this->root = $root; + $this->l = $l; + $this->config = $configHelper; + + $this->cache = null; + } + + /** + * Set the path relative to the user's root folder + * + * @param string The relative path name + * @throws NotImplementedException If the function is not implemented in the child class + */ + public function setPath(string $path) { + throw new NotImplementedException(); + } + + /** + * Get the path relative to the user's root folder + * + * @return string The relative path name + * @throws NotImplementedException If the function is not implemented in the child class + */ + public function getPath(): string { + throw new NotImplementedException(); + } + + /** + * Get the current folder + * + * @return Folder The folder + * @throws FolderNotValidException If the folder is a file or could not be generated + * @throws UserNotLoggedInException If there is no logged-in user at that time + */ + public function getFolder(): Folder { + // Ensure the cache is built. + if (is_null($this->cache)) { + $path = $this->getPath(); + + // Correct path to be relative to nc root + $path = '/' . $this->userId . '/files/' . $path; + $path = str_replace('//', '/', $path); + + + $this->cache = $this->getOrCreateFolder($path); + } + + return $this->cache; + } + + /** + * Get or Create the folder at path + * + * @param string The folder's relative path name + * @return Folder The folder at path + * @throws FolderNotValidException If the folder is a file or could not be generated + * @throws FolderNotWritableException If the folder cannot be written + */ + private function getOrCreateFolder(string $path): Folder { + try { + $node = $this->root->get($path); + } catch (NotFoundException $ex) { + try { + $node = $this->root->newFolder($path); + } catch (NotPermittedException $ex1) { + throw new FolderNotValidException( + $this->l->t('The folder cannot be created due to missing permissions.'), + 0, + $ex1 + ); + } + } + + if ($node->getType() !== FileInfo::TYPE_FOLDER) { + throw new FolderNotValidException( + $this->l->t('The configured folder is a file.') + ); + } + + if (!$node->isCreatable()) { + throw new FolderNotWritableException( + $this->l->t('User cannot create folder') + ); + } + + return $node; + } +} diff --git a/lib/Helper/MyRecipesFolderHelper.php b/lib/Helper/MyRecipesFolderHelper.php new file mode 100644 index 000000000..d3f39a050 --- /dev/null +++ b/lib/Helper/MyRecipesFolderHelper.php @@ -0,0 +1,37 @@ +config->setMyRecipesFolderName($path); + $this->cache = null; + } + + /** + * Get the path of the user's recipes relative to the user's root folder + * + * @return string The relative path name + * @throws UserNotLoggedInException If there is currently no logged in user + */ + public function getPath(): string { + $path = $this->config->getMyRecipesFolderName(); + + // TODO This was in the original code. Is it still needed? + // $path = str_replace('//', '/', $path); + + return $path; + } +} diff --git a/lib/Helper/SharedRecipesFolderHelper.php b/lib/Helper/SharedRecipesFolderHelper.php new file mode 100644 index 000000000..68ed1c5ef --- /dev/null +++ b/lib/Helper/SharedRecipesFolderHelper.php @@ -0,0 +1,37 @@ +config->setSharedRecipesFolderName($path); + $this->cache = null; + } + + /** + * Get the path of the shared recipes relative to the user's root folder + * + * @return string The relative path name + * @throws UserNotLoggedInException If there is currently no logged in user + */ + public function getPath(): string { + $path = $this->config->getSharedRecipesFolderName(); + + // TODO This was in the original code. Is it still needed? + // $path = str_replace('//', '/', $path); + + return $path; + } +} diff --git a/lib/Helper/UserConfigHelper.php b/lib/Helper/UserConfigHelper.php index a4b2efe51..b7a6ecf7f 100644 --- a/lib/Helper/UserConfigHelper.php +++ b/lib/Helper/UserConfigHelper.php @@ -41,6 +41,8 @@ public function __construct( protected const KEY_PRINT_IMAGE = 'print_image'; protected const KEY_VISIBLE_INFO_BLOCKS = 'visible_info_blocks'; protected const KEY_FOLDER = 'folder'; + protected const KEY_MY_RECIPES_FOLDER = 'my_recipes_folder'; + protected const KEY_SHARED_RECIPES_FOLDER = 'shared_recipes_folder'; /** * Checks if the user is logged in and the configuration can be obtained at all @@ -197,7 +199,7 @@ public function setVisibleInfoBlocks(array $visibleInfoBlocks): void { * Do not use this method directly. * Instead use the methods of the UserFolderHelper class * - * @return string The name of the folder within the users files + * @return string The name of the folder within the user's files * @throws UserNotLoggedInException if no user is logged in */ public function getFolderName(): string { @@ -226,4 +228,82 @@ public function getFolderName(): string { public function setFolderName(string $value): void { $this->setRawValue(self::KEY_FOLDER, $value); } + + /** + * Get the name of the user's recipe folder. + * + * If no folder is stored in the config yet, a default setting will be generated and saved. + * + * **Note:** + * Do not use this method directly. + * Instead use the methods of the MyRecipesFolderHelper class + * + * @return string The name of the folder within the user's files + * @throws UserNotLoggedInException if no user is logged in + */ + public function getMyRecipesFolderName(): string { + $rawValue = $this->getRawValue(self::KEY_MY_RECIPES_FOLDER); + + if ($rawValue === '') { + $path = getFolderName() . '/' . $this->l->t('My Recipes'); + $this->setFolderName($path); + + return $path; + } + + return $rawValue; + } + + /** + * Set the folder for the user's recipe folder. + * + * **Note:** + * Do not use this method directly. + * Instead use the methods of the MyRecipesFolderHelper class + * + * @param string $value The name of the folder within the user's files + * @throws UserNotLoggedInException if no user is logged in + */ + public function setMyRecipesFolderName(string $value): void { + $this->setRawValue(self::KEY_MY_RECIPES_FOLDER, $value); + } + + /** + * Get the name of the shared recipes folder. + * + * If no folder is stored in the config yet, a default setting will be generated and saved. + * + * **Note:** + * Do not use this method directly. + * Instead use the methods of the SharedRecipesFolderHelper class + * + * @return string The name of the folder within the user's files + * @throws UserNotLoggedInException if no user is logged in + */ + public function getSharedRecipesFolderName(): string { + $rawValue = $this->getRawValue(self::KEY_SHARED_RECIPES_FOLDER); + + if ($rawValue === '') { + $path = getFolderName() . '/' . $this->l->t('Shared Recipes'); + $this->setFolderName($path); + + return $path; + } + + return $rawValue; + } + + /** + * Set the folder for the shared recipes folder. + * + * **Note:** + * Do not use this method directly. + * Instead use the methods of the SharedRecipesFolderHelper class + * + * @param string $value The name of the folder within the user's files + * @throws UserNotLoggedInException if no user is logged in + */ + public function setSharedRecipesFolderName(string $value): void { + $this->setRawValue(self::KEY_SHARED_RECIPES_FOLDER, $value); + } } diff --git a/lib/Helper/UserFolderHelper.php b/lib/Helper/UserFolderHelper.php index 4d31229a9..f26281506 100644 --- a/lib/Helper/UserFolderHelper.php +++ b/lib/Helper/UserFolderHelper.php @@ -2,64 +2,14 @@ namespace OCA\Cookbook\Helper; -use OCA\Cookbook\Exception\UserFolderNotValidException; -use OCA\Cookbook\Exception\UserFolderNotWritableException; -use OCA\Cookbook\Exception\UserNotLoggedInException; -use OCP\Files\FileInfo; -use OCP\Files\Folder; -use OCP\Files\IRootFolder; -use OCP\Files\Node; -use OCP\Files\NotFoundException; -use OCP\Files\NotPermittedException; -use OCP\IL10N; +use OCA\Cookbook\Helper\FolderHelper; /** * This class caches the access to the user folder throughout the app. * * The user folder is the path, were all recipes are stored. */ -class UserFolderHelper { - /** - * @var UserConfigHelper - */ - private $config; - - /** - * @var IL10N - */ - private $l; - - /** - * @var ?string - */ - private $userId; - - /** - * @var IRootFolder - */ - private $root; - - /** - * The folder with all recipes or null if this is not yet cached - * - * @var ?Node - */ - private $cache; - - public function __construct( - ?string $UserId, - IRootFolder $root, - IL10N $l, - UserConfigHelper $configHelper, - ) { - $this->userId = $UserId; - $this->root = $root; - $this->l = $l; - $this->config = $configHelper; - - $this->cache = null; - } - +class UserFolderHelper extends FolderHelper { /** * Set the current path in the settings relative to the user's root folder * @@ -71,7 +21,7 @@ public function setPath(string $path) { } /** - * Get the path of the recipes relative to the user's root folder + * Get the path of the user app folder relative to the user's root folder * * @return string The relative path name * @throws UserNotLoggedInException If there is currently no logged in user @@ -84,57 +34,4 @@ public function getPath(): string { return $path; } - - /** - * Get the current folder where all recipes are stored of the user - * - * @return Folder The folder containing all recipes - * @throws UserFolderNotValidException If the saved user folder is a file or could not be generated - * @throws UserNotLoggedInException If there is no logged-in user at that time - */ - public function getFolder(): Folder { - // Ensure the cache is built. - if (is_null($this->cache)) { - $path = $this->getPath(); - - // Correct path to be relative to nc root - $path = '/' . $this->userId . '/files/' . $path; - $path = str_replace('//', '/', $path); - - - $this->cache = $this->getOrCreateFolder($path); - } - - return $this->cache; - } - - private function getOrCreateFolder(string $path): Folder { - try { - $node = $this->root->get($path); - } catch (NotFoundException $ex) { - try { - $node = $this->root->newFolder($path); - } catch (NotPermittedException $ex1) { - throw new UserFolderNotValidException( - $this->l->t('The user folder cannot be created due to missing permissions.'), - 0, - $ex1 - ); - } - } - - if ($node->getType() !== FileInfo::TYPE_FOLDER) { - throw new UserFolderNotValidException( - $this->l->t('The configured user folder is a file.') - ); - } - - if (!$node->isCreatable()) { - throw new UserFolderNotWritableException( - $this->l->t('User cannot create recipe folder') - ); - } - - return $node; - } } diff --git a/lib/Service/RecipeService.php b/lib/Service/RecipeService.php index 8abcbbdad..080ce6a07 100755 --- a/lib/Service/RecipeService.php +++ b/lib/Service/RecipeService.php @@ -216,14 +216,14 @@ public function addRecipe($json, $importedHtml = null) { $json['dateModified'] = $now; // Create/move recipe folder - $user_folder = $this->userFolder->getFolder(); + $my_recipes_folder = $this->myRecipesFolder->getFolder(); $recipe_folder = null; $recipeFolderName = $this->recipeNameHelper->getFolderName($json['name']); if (isset($json['id']) && $json['id']) { // Recipe already has an id, update it - $recipe_folder = $user_folder->getById($json['id'])[0]; + $recipe_folder = $my_recipes_folder->getById($json['id'])[0]; if (!($recipe_folder instanceof Folder)) { throw new \RuntimeException($this->il10n->t('Unexpected node received for recipe folder.')); } @@ -233,7 +233,7 @@ public function addRecipe($json, $importedHtml = null) { // The recipe is being renamed, move the folder if ($old_path !== $new_path) { - if ($user_folder->nodeExists($recipeFolderName)) { + if ($my_recipes_folder->nodeExists($recipeFolderName)) { throw new RecipeExistsException($this->il10n->t('Another recipe with that name already exists')); } @@ -244,11 +244,11 @@ public function addRecipe($json, $importedHtml = null) { // This is a new recipe, create it $json['dateCreated'] = $now; - if ($user_folder->nodeExists($recipeFolderName)) { + if ($my_recipes_folder->nodeExists($recipeFolderName)) { throw new RecipeExistsException($this->il10n->t('Another recipe with that name already exists')); } - $recipe_folder = $user_folder->newFolder($recipeFolderName); + $recipe_folder = $my_recipes_folder->newFolder($recipeFolderName); } // Write JSON file to disk @@ -367,8 +367,16 @@ public function downloadRecipe(string $url): File { * @return array */ public function getRecipeFiles() { - $user_folder = $this->userFolder->getFolder(); - $recipe_folders = $user_folder->getDirectoryListing(); + $my_recipes_folder = $this->myRecipesFolder->getFolder(); + $recipe_folders = $my_recipes_folder->getDirectoryListing(); + + $shared_recipes_folder = $this->sharedRecipesFolder->getFolder(); + $shared_recipes_folders = $shared_recipes_folder->getDirectoryListing(); + + foreach ($shared_recipes_folders as $shared_recipe_folder) { + $recipe_folders = [...$recipe_folders, ...$shared_recipe_folder->getDirectoryListing()]; + } + $recipe_files = []; foreach ($recipe_folders as $recipe_folder) { @@ -408,25 +416,56 @@ private function migrateFolderStructure() { // Restructure files if needed $user_folder = $this->userFolder->getFolder(); + $my_recipes_folder = $this->myRecipesFolder->getFolder(); + $shared_recipes_folder = $this->sharedRecipesFolder->getFolder(); - foreach ($user_folder->getDirectoryListing() as $node) { + if (!$user_folder->isSubNode($my_recipes_folder) || !$user_folder->isSubNode($shared_recipes_folder)) { + $this->logger->warning('Cannot migrate cookbook file structure as Cookbook subfolders are not within the main Cookbook folder.'); + throw $ex; + } + + if ($user_folder->getOwner() !== getCurrentUser()) + { + // moving user folder to a shared folder since it's not owned by the current user + $user_folder->move($shared_recipes_folder->getPath() . '/' . $user_folder->getOwner()->getUserId()); + $user_folder = $this->userFolder->getFolder(); + } + + $recipe_folders = [...$user_folder->getDirectoryListing(), ...$my_recipes_folder->getDirectoryListing()]; + + foreach ($recipe_folders as $node) { // Move JSON files from the user directory into its own folder if ($this->isRecipeFile($node)) { $recipe_name = str_replace('.json', '', $node->getName()); $node->move($node->getPath() . '_tmp'); - $recipe_folder = $user_folder->newFolder($recipe_name); + $recipe_folder = $my_recipes_folder->newFolder($recipe_name); $node->move($recipe_folder->getPath() . '/recipe.json'); - } elseif ($node instanceof Folder && strpos($node->getName(), '.json')) { - // Rename folders with .json extensions (this was likely caused by a migration bug) - $node->move(str_replace('.json', '', $node->getPath())); + } elseif ($node instanceof Folder) { + if(strpos($node->getName(), '.json')) { + // Rename folders with .json extensions (this was likely caused by a migration bug) + $node->move($my_recipes_folder->getPath() . '/' . str_replace('.json', '', $node->getName())); + } elseif ($this->getRecipeFileByFolderId($node->getId())) + { + if (($node->getOwner() == getCurrentUser()) && ($node->getParent() != $my_recipes_folder)) { + $node->move($my_recipes_folder->getPath() . '/' . $node->getName()); + } elseif (($node->getOwner() !== getCurrentUser()) && ($node->getParent()->getParent() != $shared_recipes_folder)) { + $other_user_folder = $shared_recipes_folder->getOrCreateFolder($node->getOwner()->GetUserId()); + if ($other_user_folder->GetOwner() == getCurrentUser()) { + $node->move($other_user_folder->getPath() . '/' . $node->getName()); + } elseif (($other_user_folder->GetOwner() !== getCurrentUser()) && ($other_user_folder->nodeExists($node->getName()))) { + $node->delete(); + } else { + $this->logger->warning('Could not determine what to do about recipe "' . $node->getName() . '". Please fix it manually (currently located at "' . $node->getPath() . '")'); + } + } + } } } } - /** * Gets all keywords from the index * diff --git a/src/components/Modals/SettingsDialog.vue b/src/components/Modals/SettingsDialog.vue index 7d96821d0..4122cae40 100644 --- a/src/components/Modals/SettingsDialog.vue +++ b/src/components/Modals/SettingsDialog.vue @@ -32,6 +32,28 @@ @click="pickRecipeFolder" /> +
  • + + +
  • +
  • + + +