diff --git a/lib/Controller/ApiBase.php b/lib/Controller/ApiBase.php index 146e7879..d8484389 100644 --- a/lib/Controller/ApiBase.php +++ b/lib/Controller/ApiBase.php @@ -25,6 +25,7 @@ namespace OCA\Memories\Controller; use OCA\Memories\AppInfo\Application; use OCA\Memories\Db\TimelineQuery; +use OCA\Memories\Db\TimelineRoot; use OCA\Memories\Db\TimelineWrite; use OCA\Memories\Exif; use OCP\App\IAppManager; @@ -88,12 +89,14 @@ class ApiBase extends Controller return $user ? $user->getUID() : ''; } - /** Get the Folder object relevant to the request */ - protected function getRequestFolder() + /** Get the TimelineRoot object relevant to the request */ + protected function getRequestRoot() { + $root = new TimelineRoot(); + // Albums have no folder if ($this->request->getParam('album')) { - return null; + return $root; } // Public shared folder @@ -103,7 +106,9 @@ class ApiBase extends Controller throw new \Exception('Share not found or invalid'); } - return $share; + $root->addFolder($share); + + return $root; } // Anything else needs a user @@ -135,7 +140,13 @@ class ApiBase extends Controller throw new \Exception('Folder not readable'); } - return $folder; + // Don't add mount points for folder view + $root->addFolder($folder); + if (null === $folderPath) { + $root->addMountPoints(); + } + + return $root; } /** diff --git a/lib/Controller/DaysController.php b/lib/Controller/DaysController.php index 5133676b..f3512cf3 100644 --- a/lib/Controller/DaysController.php +++ b/lib/Controller/DaysController.php @@ -23,9 +23,9 @@ declare(strict_types=1); namespace OCA\Memories\Controller; +use OCA\Memories\Db\TimelineRoot; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; -use OCP\Files\Folder; class DaysController extends ApiBase { @@ -42,10 +42,10 @@ class DaysController extends ApiBase $uid = $this->getUid(); // Get the folder to show - $folder = null; + $root = null; try { - $folder = $this->getRequestFolder(); + $root = $this->getRequestRoot(); } catch (\Exception $e) { return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND); } @@ -53,7 +53,7 @@ class DaysController extends ApiBase // Run actual query try { $list = $this->timelineQuery->getDays( - $folder, + $root, $uid, $this->isRecursive(), $this->isArchive(), @@ -65,7 +65,7 @@ class DaysController extends ApiBase $list = $this->timelineQuery->daysToMonths($list); } else { // Preload some day responses - $this->preloadDays($list, $uid, $folder); + $this->preloadDays($list, $uid, $root); } // Reverse response if requested. Folders still stay at top. @@ -75,7 +75,7 @@ class DaysController extends ApiBase // Add subfolder info if querying non-recursively if (!$this->isRecursive()) { - array_unshift($list, $this->getSubfoldersEntry($folder)); + array_unshift($list, $this->getSubfoldersEntry($root->getFolder($root->getOneId()))); } return new JSONResponse($list, Http::STATUS_OK); @@ -111,10 +111,10 @@ class DaysController extends ApiBase } // Get the folder to show - $folder = null; + $root = null; try { - $folder = $this->getRequestFolder(); + $root = $this->getRequestRoot(); } catch (\Exception $e) { return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND); } @@ -127,7 +127,7 @@ class DaysController extends ApiBase // Run actual query try { $list = $this->timelineQuery->getDay( - $folder, + $root, $uid, $dayIds, $this->isRecursive(), @@ -237,11 +237,11 @@ class DaysController extends ApiBase /** * Preload a few "day" at the start of "days" response. * - * @param array $days the days array - * @param string $uid User ID or blank for public shares - * @param null|Folder $folder the folder to search in + * @param array $days the days array + * @param string $uid User ID or blank for public shares + * @param TimelineRoot $root the root folder */ - private function preloadDays(array &$days, string $uid, &$folder) + private function preloadDays(array &$days, string $uid, TimelineRoot &$root) { $transforms = $this->getTransformations(false); $preloaded = 0; @@ -263,7 +263,7 @@ class DaysController extends ApiBase if (\count($preloadDayIds) > 0) { $allDetails = $this->timelineQuery->getDay( - $folder, + $root, $uid, $preloadDayIds, $this->isRecursive(), diff --git a/lib/Controller/FacesController.php b/lib/Controller/FacesController.php index f4dfd097..c768129e 100644 --- a/lib/Controller/FacesController.php +++ b/lib/Controller/FacesController.php @@ -49,14 +49,14 @@ class FacesController extends ApiBase } // If this isn't the timeline folder then things aren't going to work - $folder = $this->getRequestFolder(); - if (null === $folder) { + $root = $this->getRequestRoot(); + if ($root->isEmpty()) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } // Run actual query $list = $this->timelineQuery->getFaces( - $folder, + $root, ); return new JSONResponse($list, Http::STATUS_OK); @@ -84,22 +84,23 @@ class FacesController extends ApiBase } // Get folder to search for - $folder = $this->getRequestFolder(); - if (null === $folder) { + $root = $this->getRequestRoot(); + if ($root->isEmpty()) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } // Run actual query - $detections = $this->timelineQuery->getFacePreviewDetection($folder, (int) $id); + $detections = $this->timelineQuery->getFacePreviewDetection($root, (int) $id); if (null === $detections || 0 === \count($detections)) { return new DataResponse([], Http::STATUS_NOT_FOUND); } // Find the first detection that has a preview $preview = null; + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); foreach ($detections as &$detection) { // Get the file (also checks permissions) - $files = $folder->getById($detection['file_id']); + $files = $userFolder->getById($detection['file_id']); if (0 === \count($files) || FileInfo::TYPE_FILE !== $files[0]->getType()) { continue; } diff --git a/lib/Controller/TagsController.php b/lib/Controller/TagsController.php index d4c3c667..c689253d 100644 --- a/lib/Controller/TagsController.php +++ b/lib/Controller/TagsController.php @@ -46,14 +46,14 @@ class TagsController extends ApiBase } // If this isn't the timeline folder then things aren't going to work - $folder = $this->getRequestFolder(); - if (null === $folder) { + $root = $this->getRequestRoot(); + if ($root->isEmpty()) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } // Run actual query $list = $this->timelineQuery->getTags( - $folder, + $root, ); return new JSONResponse($list, Http::STATUS_OK); @@ -77,8 +77,8 @@ class TagsController extends ApiBase } // If this isn't the timeline folder then things aren't going to work - $folder = $this->getRequestFolder(); - if (null === $folder) { + $root = $this->getRequestRoot(); + if ($root->isEmpty()) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } @@ -88,7 +88,7 @@ class TagsController extends ApiBase // Run actual query $list = $this->timelineQuery->getTagPreviews( $tagName, - $folder, + $root, ); return new JSONResponse($list, Http::STATUS_OK); diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index 9b74ca5f..40526edf 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace OCA\Memories\Db; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\Files\Folder; use OCP\IDBConnection; const CTE_FOLDERS = // CTE to get all folders recursively in the given top folders excluding archive @@ -73,21 +72,18 @@ trait TimelineQueryDays { protected IDBConnection $connection; - /** Map of rootid => mount point */ - private $topFolderPaths = []; - /** * Get the days response from the database for the timeline. * - * @param null|Folder $folder The folder to get the days from - * @param bool $recursive Whether to get the days recursively - * @param bool $archive Whether to get the days only from the archive folder - * @param array $queryTransforms An array of query transforms to apply to the query + * @param TimelineRoot $root The root to get the days from + * @param bool $recursive Whether to get the days recursively + * @param bool $archive Whether to get the days only from the archive folder + * @param array $queryTransforms An array of query transforms to apply to the query * * @return array The days response */ public function getDays( - &$folder, + TimelineRoot &$root, string $uid, bool $recursive, bool $archive, @@ -100,7 +96,7 @@ trait TimelineQueryDays $query->select('m.dayid', $count) ->from('memories', 'm') ; - $query = $this->joinFilecache($query, $folder, $recursive, $archive); + $query = $this->joinFilecache($query, $root, $recursive, $archive); // Group and sort by dayid $query->groupBy('m.dayid') @@ -120,18 +116,18 @@ trait TimelineQueryDays /** * Get the day response from the database for the timeline. * - * @param null|Folder $folder The folder to get the day from - * @param string $uid The user id - * @param int[] $day_ids The day ids to fetch - * @param bool $recursive If the query should be recursive - * @param bool $archive If the query should include only the archive folder - * @param array $queryTransforms The query transformations to apply - * @param mixed $day_ids + * @param TimelineRoot $root The root to get the day from + * @param string $uid The user id + * @param int[] $day_ids The day ids to fetch + * @param bool $recursive If the query should be recursive + * @param bool $archive If the query should include only the archive folder + * @param array $queryTransforms The query transformations to apply + * @param mixed $day_ids * * @return array An array of day responses */ public function getDay( - &$folder, + TimelineRoot &$root, string $uid, $day_ids, bool $recursive, @@ -151,7 +147,7 @@ trait TimelineQueryDays ; // JOIN with filecache for existing files - $query = $this->joinFilecache($query, $folder, $recursive, $archive); + $query = $this->joinFilecache($query, $root, $recursive, $archive); $query->addSelect('f.etag', 'f.path', 'f.name AS basename'); // JOIN with mimetypes to get the mimetype @@ -180,7 +176,7 @@ trait TimelineQueryDays $rows = $cursor->fetchAll(); $cursor->closeCursor(); - return $this->processDay($rows, $uid, $folder); + return $this->processDay($rows, $uid, $root); } /** @@ -200,12 +196,8 @@ trait TimelineQueryDays /** * Process the single day response. - * - * @param array $day - * @param string $uid User or blank if not logged in - * @param null|Folder $folder */ - private function processDay(&$day, $uid, $folder) + private function processDay(array &$day, string $uid, TimelineRoot &$root) { /** * Path entry in database for folder. @@ -225,15 +217,15 @@ trait TimelineQueryDays */ $defaultRootId = 0; - if (null !== $folder) { + if (!$root->isEmpty()) { // Get root id of the top folder - $defaultRootId = $folder->getId(); + $defaultRootId = $root->getOneId(); // No way to get the internal path from the folder $query = $this->connection->getQueryBuilder(); $query->select('fileid', 'path') ->from('filecache') - ->where($query->expr()->in('fileid', $query->createNamedParameter(array_keys($this->topFolderPaths), IQueryBuilder::PARAM_INT_ARRAY))) + ->where($query->expr()->in('fileid', $query->createNamedParameter($root->getIds(), IQueryBuilder::PARAM_INT_ARRAY))) ; $paths = $query->executeQuery()->fetchAll(); foreach ($paths as &$path) { @@ -244,7 +236,7 @@ trait TimelineQueryDays // getPath looks like /user/files/... but we want /files/user/... // Split at / and swap these // For public shares, we just give the relative path - if (!empty($uid) && ($actualPath = $this->topFolderPaths[$fileid])) { + if (!empty($uid) && ($actualPath = $root->getFolderPath($fileid))) { $actualPath = explode('/', $actualPath); if (\count($actualPath) >= 3) { $tmp = $actualPath[1]; @@ -317,10 +309,11 @@ trait TimelineQueryDays */ private function addSubfolderJoinParams( IQueryBuilder &$query, + TimelineRoot &$root, bool $archive ) { // Add query parameters - $query->setParameter('topFolderIds', array_keys($this->topFolderPaths), IQueryBuilder::PARAM_INT_ARRAY); + $query->setParameter('topFolderIds', $root->getIds(), IQueryBuilder::PARAM_INT_ARRAY); $query->setParameter('cteFoldersArchive', $archive, IQueryBuilder::PARAM_BOOL); } @@ -328,44 +321,32 @@ trait TimelineQueryDays * Inner join with oc_filecache. * * @param IQueryBuilder $query Query builder - * @param null|Folder $folder Either the top folder or null for all + * @param TimelineRoot $root Either the top folder or null for all * @param bool $recursive Whether to get the days recursively * @param bool $archive Whether to get the days only from the archive folder */ private function joinFilecache( IQueryBuilder &$query, - &$folder, + TimelineRoot &$root, bool $recursive, bool $archive ) { // Join with memories $baseOp = $query->expr()->eq('f.fileid', 'm.fileid'); - if (null === $folder) { + if ($root->isEmpty()) { return $query->innerJoin('m', 'filecache', 'f', $baseOp); } - // Create top folders paths for later processing - $this->topFolderPaths = []; - $this->topFolderPaths[$folder->getId()] = $folder->getPath(); - // Filter by folder (recursive or otherwise) $pathOp = null; if ($recursive) { - // Add mountpoints recursively - $this->mounts = \OC\Files\Filesystem::getMountManager()->findIn($folder->getPath()); - foreach ($this->mounts as &$mount) { - $id = $mount->getStorageRootId(); - $path = $mount->getMountPoint(); - $this->topFolderPaths[$id] = $path; - } - // Join with folders CTE - $this->addSubfolderJoinParams($query, $archive); + $this->addSubfolderJoinParams($query, $root, $archive); $query->innerJoin('f', 'cte_folders', 'cte_f', $query->expr()->eq('f.parent', 'cte_f.fileid')); $query->addSelect('cte_f.rootid'); } else { // If getting non-recursively folder only check for parent - $pathOp = $query->expr()->eq('f.parent', $query->createNamedParameter($folder->getId(), IQueryBuilder::PARAM_INT)); + $pathOp = $query->expr()->eq('f.parent', $query->createNamedParameter($root->getOneId(), IQueryBuilder::PARAM_INT)); } return $query->innerJoin('m', 'filecache', 'f', $query->expr()->andX( diff --git a/lib/Db/TimelineQueryFaces.php b/lib/Db/TimelineQueryFaces.php index ed01fd2e..4cf6f6f2 100644 --- a/lib/Db/TimelineQueryFaces.php +++ b/lib/Db/TimelineQueryFaces.php @@ -47,7 +47,7 @@ trait TimelineQueryFaces ); } - public function getFaces(Folder $folder) + public function getFaces(TimelineRoot &$root) { $query = $this->connection->getQueryBuilder(); @@ -62,7 +62,7 @@ trait TimelineQueryFaces $query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id')); // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query, $folder, true, false); + $query = $this->joinFilecache($query, $root, true, false); // GROUP by ID of face cluster $query->groupBy('rfc.id'); @@ -87,7 +87,7 @@ trait TimelineQueryFaces return $faces; } - public function getFacePreviewDetection(Folder &$folder, int $id) + public function getFacePreviewDetection(TimelineRoot &$root, int $id) { $query = $this->connection->getQueryBuilder(); @@ -109,7 +109,7 @@ trait TimelineQueryFaces $query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id')); // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query, $folder, true, false); + $query = $this->joinFilecache($query, $root, true, false); // LIMIT results $query->setMaxResults(15); diff --git a/lib/Db/TimelineQueryFolders.php b/lib/Db/TimelineQueryFolders.php index 26910ece..347e7e64 100644 --- a/lib/Db/TimelineQueryFolders.php +++ b/lib/Db/TimelineQueryFolders.php @@ -19,7 +19,9 @@ trait TimelineQueryFolders $query->select('f.fileid', 'f.etag')->from('memories', 'm'); // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query, $folder, true, false); + $root = new TimelineRoot(); + $root->addFolder($folder); + $query = $this->joinFilecache($query, $root, true, false); // ORDER descending by fileid $query->orderBy('f.fileid', 'DESC'); diff --git a/lib/Db/TimelineQueryTags.php b/lib/Db/TimelineQueryTags.php index 8a3b9bfd..4c4224eb 100644 --- a/lib/Db/TimelineQueryTags.php +++ b/lib/Db/TimelineQueryTags.php @@ -38,7 +38,7 @@ trait TimelineQueryTags )); } - public function getTags(Folder $folder) + public function getTags(TimelineRoot &$root) { $query = $this->connection->getQueryBuilder(); @@ -58,7 +58,7 @@ trait TimelineQueryTags $query->innerJoin('stom', 'memories', 'm', $query->expr()->eq('m.objectid', 'stom.objectid')); // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query, $folder, true, false); + $query = $this->joinFilecache($query, $root, true, false); // GROUP and ORDER by tag name $query->groupBy('st.id'); @@ -78,7 +78,7 @@ trait TimelineQueryTags return $tags; } - public function getTagPreviews(string $tagName, Folder &$folder) + public function getTagPreviews(string $tagName, TimelineRoot &$root) { $query = $this->connection->getQueryBuilder(); $tagId = $this->getSystemTagId($query, $tagName); @@ -99,7 +99,7 @@ trait TimelineQueryTags $query->innerJoin('stom', 'memories', 'm', $query->expr()->eq('m.objectid', 'stom.objectid')); // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query, $folder, true, false); + $query = $this->joinFilecache($query, $root, true, false); // MAX 4 $query->setMaxResults(4); diff --git a/lib/Db/TimelineRoot.php b/lib/Db/TimelineRoot.php new file mode 100644 index 00000000..04490a73 --- /dev/null +++ b/lib/Db/TimelineRoot.php @@ -0,0 +1,67 @@ +getId(); + $folderPath = $folder->getPath(); + $this->folders[$id] = $folder; + $this->folderPaths[$id] = $folderPath; + } + + // Add mountpoints recursively + public function addMountPoints() + { + $mp = []; + foreach ($this->folderPaths as $id => $folderPath) { + $mounts = \OC\Files\Filesystem::getMountManager()->findIn($folderPath); + foreach ($mounts as &$mount) { + $id = $mount->getStorageRootId(); + $path = $mount->getMountPoint(); + $mp[$id] = $path; + } + } + $this->folderPaths += $mp; + } + + public function getFolderPath(int $id) + { + return $this->folderPaths[$id]; + } + + public function getIds() + { + return array_keys($this->folderPaths); + } + + public function getOneId() + { + return array_key_first($this->folders); + } + + public function getFolder(int $id) + { + return $this->folders[$id]; + } + + public function isEmpty() + { + return empty($this->folderPaths); + } +}