memories/lib/Db/TimelineQueryDays.php

379 lines
12 KiB
PHP
Raw Normal View History

2022-09-11 02:05:04 +00:00
<?php
2022-10-19 17:10:36 +00:00
2022-09-11 02:05:04 +00:00
declare(strict_types=1);
namespace OCA\Memories\Db;
2022-11-24 15:52:19 +00:00
use OCA\Memories\Exif;
2022-09-11 02:05:04 +00:00
use OCP\DB\QueryBuilder\IQueryBuilder;
2022-10-19 17:10:36 +00:00
use OCP\IDBConnection;
2022-09-11 02:05:04 +00:00
const CTE_FOLDERS = // CTE to get all folders recursively in the given top folders excluding archive
2022-11-16 09:49:03 +00:00
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, rootid) AS (
2022-10-27 16:19:25 +00:00
SELECT
2022-11-16 05:05:11 +00:00
f.fileid,
f.fileid AS rootid
2022-10-27 16:19:25 +00:00
FROM
*PREFIX*filecache f
WHERE
f.fileid IN (:topFolderIds)
2022-10-27 16:19:25 +00:00
UNION ALL
SELECT
2022-11-16 05:05:11 +00:00
f.fileid,
c.rootid
2022-10-27 16:19:25 +00:00
FROM
*PREFIX*filecache f
2022-11-16 09:49:03 +00:00
INNER JOIN *PREFIX*cte_folders_all c
2022-10-27 16:19:25 +00:00
ON (f.parent = c.fileid
2022-11-01 05:59:14 +00:00
AND f.mimetype = (SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = \'httpd/unix-directory\')
AND f.name <> \'.archive\'
2022-10-27 16:19:25 +00:00
)
2022-11-16 09:49:03 +00:00
), *PREFIX*cte_folders AS (
SELECT
fileid,
2023-01-20 18:50:48 +00:00
MIN(rootid) AS rootid
2022-11-16 09:49:03 +00:00
FROM
*PREFIX*cte_folders_all
GROUP BY
2023-01-20 18:50:48 +00:00
fileid
2022-10-27 16:19:25 +00:00
)';
const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the given top folders
2022-11-16 05:22:40 +00:00
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, name, rootid) AS (
SELECT
2022-11-16 05:22:40 +00:00
f.fileid,
f.name,
f.fileid AS rootid
FROM
*PREFIX*filecache f
WHERE
f.fileid IN (:topFolderIds)
UNION ALL
SELECT
2022-11-16 05:22:40 +00:00
f.fileid,
f.name,
c.rootid
FROM
*PREFIX*filecache f
INNER JOIN *PREFIX*cte_folders_all c
ON (f.parent = c.fileid
AND f.mimetype = (SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = \'httpd/unix-directory\')
)
2022-11-16 05:22:40 +00:00
), *PREFIX*cte_folders(fileid, rootid) AS (
SELECT
2022-11-16 05:22:40 +00:00
cfa.fileid,
2022-11-16 09:49:03 +00:00
MIN(cfa.rootid) AS rootid
FROM
2022-11-16 05:22:40 +00:00
*PREFIX*cte_folders_all cfa
WHERE
2022-11-16 05:22:40 +00:00
cfa.name = \'.archive\'
2022-11-16 09:49:03 +00:00
GROUP BY
cfa.fileid
UNION ALL
SELECT
2022-11-16 05:22:40 +00:00
f.fileid,
c.rootid
FROM
*PREFIX*filecache f
INNER JOIN *PREFIX*cte_folders c
ON (f.parent = c.fileid)
)';
2022-10-19 17:10:36 +00:00
trait TimelineQueryDays
{
2022-09-11 02:05:04 +00:00
protected IDBConnection $connection;
/**
2022-10-19 17:10:36 +00:00
* Get the days response from the database for the timeline.
*
2022-11-16 07:45:01 +00:00
* @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
2022-10-06 19:24:45 +00:00
*
* @return array The days response
2022-09-11 02:05:04 +00:00
*/
public function getDays(
2022-11-16 07:45:01 +00:00
TimelineRoot &$root,
2022-09-24 01:54:14 +00:00
string $uid,
bool $recursive,
2022-09-25 23:02:26 +00:00
bool $archive,
2022-09-12 01:33:38 +00:00
array $queryTransforms = []
2022-09-12 01:06:16 +00:00
): array {
2022-09-11 02:05:04 +00:00
$query = $this->connection->getQueryBuilder();
2022-09-12 01:03:40 +00:00
2022-09-24 01:54:14 +00:00
// Get all entries also present in filecache
$count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count');
$query->select('m.dayid', $count)
->from('memories', 'm')
2022-10-19 17:10:36 +00:00
;
2022-11-16 07:45:01 +00:00
$query = $this->joinFilecache($query, $root, $recursive, $archive);
2022-09-24 01:54:14 +00:00
// Group and sort by dayid
$query->groupBy('m.dayid')
2022-10-19 17:10:36 +00:00
->orderBy('m.dayid', 'DESC')
;
2022-09-11 02:05:04 +00:00
2022-09-12 01:33:38 +00:00
// Apply all transformations
2022-10-06 21:19:47 +00:00
$this->applyAllTransforms($queryTransforms, $query, $uid);
2022-09-12 01:33:38 +00:00
2022-10-27 16:19:25 +00:00
$cursor = $this->executeQueryWithCTEs($query);
2022-09-25 11:30:28 +00:00
$rows = $cursor->fetchAll();
$cursor->closeCursor();
2022-10-19 17:10:36 +00:00
2022-09-11 02:05:04 +00:00
return $this->processDays($rows);
}
2022-10-06 19:24:45 +00:00
/**
2022-10-19 17:10:36 +00:00
* Get the day response from the database for the timeline.
*
2022-11-16 07:45:01 +00:00
* @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
2022-10-19 17:10:36 +00:00
*
2022-10-06 19:24:45 +00:00
* @return array An array of day responses
2022-09-11 02:05:04 +00:00
*/
2022-09-24 01:54:14 +00:00
public function getDay(
2022-11-16 07:45:01 +00:00
TimelineRoot &$root,
2022-09-24 01:54:14 +00:00
string $uid,
2022-11-24 02:28:34 +00:00
?array $day_ids,
2022-09-24 01:54:14 +00:00
bool $recursive,
2022-09-25 23:02:26 +00:00
bool $archive,
2022-09-24 01:54:14 +00:00
array $queryTransforms = []
): array {
2022-09-11 02:05:04 +00:00
$query = $this->connection->getQueryBuilder();
2022-09-24 01:54:14 +00:00
// Get all entries also present in filecache
$fileid = $query->createFunction('DISTINCT m.fileid');
// We don't actually use m.datetaken here, but postgres
// needs that all fields in ORDER BY are also in SELECT
// when using DISTINCT on selected fields
2023-03-10 17:30:56 +00:00
$query->select($fileid, ...TimelineQuery::TIMELINE_SELECT)
2022-09-24 01:54:14 +00:00
->from('memories', 'm')
2022-10-19 17:10:36 +00:00
;
2022-10-29 01:11:58 +00:00
// JOIN with filecache for existing files
2022-11-16 07:45:01 +00:00
$query = $this->joinFilecache($query, $root, $recursive, $archive);
2022-10-29 01:11:58 +00:00
2022-11-16 09:17:01 +00:00
// SELECT rootid if not a single folder
2022-11-16 10:35:17 +00:00
if ($recursive && !$root->isEmpty()) {
2022-11-16 10:30:51 +00:00
$query->addSelect('cte_f.rootid');
}
2022-11-16 09:17:01 +00:00
2022-10-29 01:11:58 +00:00
// JOIN with mimetypes to get the mimetype
$query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id'));
2022-10-06 22:01:28 +00:00
// Filter by dayid unless wildcard
2022-10-19 17:10:36 +00:00
if (null !== $day_ids) {
2022-10-06 22:01:28 +00:00
$query->andWhere($query->expr()->in('m.dayid', $query->createNamedParameter($day_ids, IQueryBuilder::PARAM_INT_ARRAY)));
} else {
// Limit wildcard to 100 results
$query->setMaxResults(100);
}
2022-09-24 01:54:14 +00:00
// Add favorite field
$this->addFavoriteTag($query, $uid);
// Group and sort by date taken
$query->orderBy('m.datetaken', 'DESC');
2022-10-16 19:18:31 +00:00
$query->addOrderBy('m.fileid', 'DESC'); // tie-breaker
2022-09-24 01:54:14 +00:00
// Apply all transformations
2022-10-06 21:19:47 +00:00
$this->applyAllTransforms($queryTransforms, $query, $uid);
2022-09-11 02:05:04 +00:00
2022-10-27 16:19:25 +00:00
$cursor = $this->executeQueryWithCTEs($query);
2022-09-25 11:30:28 +00:00
$rows = $cursor->fetchAll();
$cursor->closeCursor();
2022-10-19 17:10:36 +00:00
2022-11-16 07:45:01 +00:00
return $this->processDay($rows, $uid, $root);
2022-09-11 02:05:04 +00:00
}
2022-10-19 17:10:36 +00:00
/**
* Process the days response.
*
* @param array $days
*/
private function processDays(&$days)
{
foreach ($days as &$row) {
2022-10-19 17:15:14 +00:00
$row['dayid'] = (int) $row['dayid'];
$row['count'] = (int) $row['count'];
2022-10-19 17:10:36 +00:00
}
return $days;
}
/**
* Process the single day response.
*/
2022-11-16 07:45:01 +00:00
private function processDay(array &$day, string $uid, TimelineRoot &$root)
2022-10-19 17:10:36 +00:00
{
2022-11-16 05:05:11 +00:00
/**
* Path entry in database for folder.
* We need to splice this from the start of the file path.
*/
$internalPaths = [];
/**
* DAV paths for the folders.
* We need to prefix this to the start of the file path.
*/
$davPaths = [];
/**
* The root folder id for the folder.
2022-11-16 05:45:52 +00:00
* We fallback to this if rootid is not found.
2022-11-16 05:05:11 +00:00
*/
$defaultRootId = 0;
2022-11-16 07:45:01 +00:00
if (!$root->isEmpty()) {
2022-11-16 05:05:11 +00:00
// Get root id of the top folder
2022-11-16 07:45:01 +00:00
$defaultRootId = $root->getOneId();
2022-11-16 05:05:11 +00:00
2022-10-29 01:11:58 +00:00
// No way to get the internal path from the folder
$query = $this->connection->getQueryBuilder();
2022-11-16 05:05:11 +00:00
$query->select('fileid', 'path')
2022-10-29 01:11:58 +00:00
->from('filecache')
2022-11-16 07:45:01 +00:00
->where($query->expr()->in('fileid', $query->createNamedParameter($root->getIds(), IQueryBuilder::PARAM_INT_ARRAY)))
2022-10-29 01:11:58 +00:00
;
2022-11-16 05:05:11 +00:00
$paths = $query->executeQuery()->fetchAll();
foreach ($paths as &$path) {
$fileid = (int) $path['fileid'];
$internalPaths[$fileid] = $path['path'];
// Get DAV path.
// getPath looks like /user/files/... but we want /files/user/...
// Split at / and swap these
// For public shares, we just give the relative path
2022-11-16 07:45:01 +00:00
if (!empty($uid) && ($actualPath = $root->getFolderPath($fileid))) {
2022-11-16 05:05:11 +00:00
$actualPath = explode('/', $actualPath);
if (\count($actualPath) >= 3) {
$tmp = $actualPath[1];
$actualPath[1] = $actualPath[2];
$actualPath[2] = $tmp;
$davPath = implode('/', $actualPath);
2022-11-24 15:52:19 +00:00
$davPaths[$fileid] = Exif::removeExtraSlash('/'.$davPath.'/');
2022-11-16 05:05:11 +00:00
}
2022-10-29 01:29:28 +00:00
}
2022-10-29 01:11:58 +00:00
}
}
2022-10-29 00:25:39 +00:00
2022-10-19 17:10:36 +00:00
foreach ($day as &$row) {
// Convert field types
2022-10-19 17:15:14 +00:00
$row['fileid'] = (int) $row['fileid'];
$row['isvideo'] = (int) $row['isvideo'];
2022-11-10 03:48:03 +00:00
$row['video_duration'] = (int) $row['video_duration'];
2022-10-19 17:15:14 +00:00
$row['dayid'] = (int) $row['dayid'];
$row['w'] = (int) $row['w'];
$row['h'] = (int) $row['h'];
2022-10-19 17:10:36 +00:00
if (!$row['isvideo']) {
2022-11-10 06:19:44 +00:00
unset($row['isvideo'], $row['video_duration']);
2022-10-19 17:10:36 +00:00
}
if ($row['categoryid']) {
$row['isfavorite'] = 1;
}
unset($row['categoryid']);
2022-11-22 11:31:31 +00:00
if (!$row['liveid']) {
unset($row['liveid']);
}
2022-10-19 17:10:36 +00:00
2022-10-29 00:25:39 +00:00
// Check if path exists and starts with basePath and remove
if (isset($row['path']) && !empty($row['path'])) {
2022-11-16 08:23:57 +00:00
$rootId = \array_key_exists('rootid', $row) ? $row['rootid'] : $defaultRootId;
$basePath = $internalPaths[$rootId] ?? '#__#';
2022-12-03 08:14:29 +00:00
$davPath = (\array_key_exists($rootId, $davPaths) ? $davPaths[$rootId] : null) ?: '';
2022-11-16 05:05:11 +00:00
2022-10-29 00:25:39 +00:00
if (0 === strpos($row['path'], $basePath)) {
2022-11-24 15:52:19 +00:00
$rpath = substr($row['path'], \strlen($basePath));
$row['filename'] = Exif::removeExtraSlash($davPath.$rpath);
2022-10-29 00:25:39 +00:00
}
2022-11-16 05:05:11 +00:00
2022-10-29 00:25:39 +00:00
unset($row['path']);
}
2022-10-19 17:10:36 +00:00
// All transform processing
2022-12-08 21:00:53 +00:00
$this->processPeopleRecognizeDetection($row);
$this->processFaceRecognitionDetection($row);
2022-12-05 04:04:48 +00:00
// We don't need these fields
2022-12-05 04:05:05 +00:00
unset($row['datetaken'], $row['rootid']);
2022-10-19 17:10:36 +00:00
}
return $day;
}
2022-10-27 19:54:51 +00:00
private function executeQueryWithCTEs(IQueryBuilder &$query, string $psql = '')
2022-10-27 16:19:25 +00:00
{
2022-10-27 19:54:51 +00:00
$sql = empty($psql) ? $query->getSQL() : $psql;
2022-10-27 16:19:25 +00:00
$params = $query->getParameters();
$types = $query->getParameterTypes();
// Get SQL
2022-11-16 08:16:07 +00:00
$CTE_SQL = \array_key_exists('cteFoldersArchive', $params) && $params['cteFoldersArchive']
2023-01-20 18:50:48 +00:00
? CTE_FOLDERS_ARCHIVE
: CTE_FOLDERS;
2022-10-27 16:19:25 +00:00
// Add WITH clause if needed
if (false !== strpos($sql, 'cte_folders')) {
$sql = $CTE_SQL.' '.$sql;
2022-10-27 16:19:25 +00:00
}
return $this->connection->executeQuery($sql, $params, $types);
}
2022-10-26 16:29:32 +00:00
/**
2022-10-26 17:06:45 +00:00
* Get all folders inside a top folder.
2022-10-26 16:29:32 +00:00
*/
2022-10-27 19:54:51 +00:00
private function addSubfolderJoinParams(
2022-10-27 16:19:25 +00:00
IQueryBuilder &$query,
2022-11-16 07:45:01 +00:00
TimelineRoot &$root,
2022-10-19 17:10:36 +00:00
bool $archive
) {
2022-10-27 16:19:25 +00:00
// Add query parameters
2022-11-16 07:45:01 +00:00
$query->setParameter('topFolderIds', $root->getIds(), IQueryBuilder::PARAM_INT_ARRAY);
$query->setParameter('cteFoldersArchive', $archive, IQueryBuilder::PARAM_BOOL);
2022-10-26 16:29:32 +00:00
}
2022-10-19 17:10:36 +00:00
2022-10-26 16:29:32 +00:00
/**
2022-10-27 16:19:25 +00:00
* Inner join with oc_filecache.
2022-10-26 16:29:32 +00:00
*
2022-10-27 16:19:25 +00:00
* @param IQueryBuilder $query Query builder
2022-11-16 07:45:01 +00:00
* @param TimelineRoot $root Either the top folder or null for all
2022-10-27 16:19:25 +00:00
* @param bool $recursive Whether to get the days recursively
* @param bool $archive Whether to get the days only from the archive folder
2022-10-26 16:29:32 +00:00
*/
2022-10-27 16:19:25 +00:00
private function joinFilecache(
2022-10-26 16:29:32 +00:00
IQueryBuilder &$query,
2022-11-16 07:45:01 +00:00
TimelineRoot &$root,
2022-10-26 16:29:32 +00:00
bool $recursive,
bool $archive
) {
2022-10-26 23:20:28 +00:00
// Join with memories
$baseOp = $query->expr()->eq('f.fileid', 'm.fileid');
2022-11-16 07:45:01 +00:00
if ($root->isEmpty()) {
2022-10-27 16:19:25 +00:00
return $query->innerJoin('m', 'filecache', 'f', $baseOp);
2022-10-26 23:20:28 +00:00
}
// Filter by folder (recursive or otherwise)
$pathOp = null;
2022-10-26 16:29:32 +00:00
if ($recursive) {
2022-10-27 16:19:25 +00:00
// Join with folders CTE
2022-11-16 07:45:01 +00:00
$this->addSubfolderJoinParams($query, $root, $archive);
2022-10-27 19:54:51 +00:00
$query->innerJoin('f', 'cte_folders', 'cte_f', $query->expr()->eq('f.parent', 'cte_f.fileid'));
2022-10-19 17:10:36 +00:00
} else {
// If getting non-recursively folder only check for parent
2022-11-16 07:45:01 +00:00
$pathOp = $query->expr()->eq('f.parent', $query->createNamedParameter($root->getOneId(), IQueryBuilder::PARAM_INT));
2022-10-19 17:10:36 +00:00
}
2022-10-27 16:19:25 +00:00
return $query->innerJoin('m', 'filecache', 'f', $query->expr()->andX(
2022-10-26 23:20:28 +00:00
$baseOp,
$pathOp,
2022-10-27 16:19:25 +00:00
));
2022-10-19 17:10:36 +00:00
}
2022-09-11 02:05:04 +00:00
}