diff --git a/lib/Controller/ApiBase.php b/lib/Controller/ApiBase.php index ac9c9bce..4d3206b5 100644 --- a/lib/Controller/ApiBase.php +++ b/lib/Controller/ApiBase.php @@ -103,6 +103,10 @@ class ApiBase extends Controller // Public shared folder if ($share = $this->getShareNode()) { // can throw + if (!$share instanceof Folder) { + throw new \Exception('Share is not a folder'); + } + $root->addFolder($share); return $root; @@ -229,7 +233,14 @@ class ApiBase extends Controller if ($share = $this->getShareNode()) { // Public shares may allow editing // Just use the same permissions as the share - return $this->getOneFileFromFolder($share, $id, $share->getPermissions()); + if ($share instanceof File) { + return $share; + } + if ($share instanceof Folder) { + return $this->getOneFileFromFolder($share, $id, $share->getPermissions()); + } + + return null; } } catch (\Exception $e) { } @@ -301,7 +312,7 @@ class ApiBase extends Controller // Get node from share $node = $share->getNode(); // throws exception if not found - if (!$node instanceof Folder || !$node->isReadable() || !$node->isShareable()) { + if (!$node->isReadable() || !$node->isShareable()) { throw new \Exception('Share not found or invalid'); } diff --git a/lib/Controller/PublicController.php b/lib/Controller/PublicController.php index ff136914..29d1a745 100644 --- a/lib/Controller/PublicController.php +++ b/lib/Controller/PublicController.php @@ -3,6 +3,7 @@ namespace OCA\Memories\Controller; use OCA\Memories\AppInfo\Application; +use OCA\Memories\Db\TimelineQuery; use OCP\App\IAppManager; use OCP\AppFramework\AuthPublicShareController; use OCP\AppFramework\Http\Template\PublicTemplateResponse; @@ -12,6 +13,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\IConfig; +use OCP\IDBConnection; use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; @@ -31,6 +33,7 @@ class PublicController extends AuthPublicShareController protected IShareManager $shareManager; protected IUserManager $userManager; protected IAppManager $appManager; + protected IDBConnection $db; protected IConfig $config; protected IShare $share; @@ -47,6 +50,7 @@ class PublicController extends AuthPublicShareController IShareManager $shareManager, IUserManager $userManager, IAppManager $appManager, + IDBConnection $db, IConfig $config ) { parent::__construct($AppName, $request, $session, $urlGenerator); @@ -57,6 +61,7 @@ class PublicController extends AuthPublicShareController $this->shareManager = $shareManager; $this->userManager = $userManager; $this->appManager = $appManager; + $this->db = $db; $this->config = $config; } @@ -106,11 +111,6 @@ class PublicController extends AuthPublicShareController throw new NotFoundException(); } - if (!($share->getNode() instanceof \OCP\Files\Folder)) { - // TODO: single file share - throw new NotFoundException(); - } - // Redirect to main app if user owns this share $this->redirectIfOwned($share); @@ -124,8 +124,14 @@ class PublicController extends AuthPublicShareController // Share info $this->initialState->provideInitialState('no_download', $share->getHideDownload()); + // Share file id only if not a folder + $node = $share->getNode(); + if ($node instanceof \OCP\Files\File) { + $this->initialState->provideInitialState('single_item', $this->getSingleItemInitialState($node)); + } + $response = new PublicTemplateResponse($this->appName, 'main'); - $response->setHeaderTitle($share->getNode()->getName()); + $response->setHeaderTitle($node->getName()); $response->setFooterVisible(false); // wth is that anyway? $response->setContentSecurityPolicy(PageController::getCSP()); $response->cacheFor(0); @@ -241,4 +247,13 @@ class PublicController extends AuthPublicShareController exit; } + + /** Get initial state of single item */ + private function getSingleItemInitialState(\OCP\Files\File $file): string + { + $timelineQuery = new TimelineQuery($this->db); + $photo = $timelineQuery->getSingleItem($file->getId()); + + return json_encode($photo); + } } diff --git a/lib/Db/TimelineQuery.php b/lib/Db/TimelineQuery.php index acd91f9d..c825d9f9 100644 --- a/lib/Db/TimelineQuery.php +++ b/lib/Db/TimelineQuery.php @@ -18,8 +18,14 @@ class TimelineQuery use TimelineQueryPeopleFaceRecognition; use TimelineQueryPeopleRecognize; use TimelineQueryPlaces; + use TimelineQuerySingleItem; use TimelineQueryTags; + public const TIMELINE_SELECT = [ + 'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h', 'm.liveid', + 'f.etag', 'f.path', 'f.name AS basename', 'mimetypes.mimetype', + ]; + protected IDBConnection $connection; public function __construct(IDBConnection $connection) diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index 1e238863..c8ec60c1 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -153,13 +153,12 @@ trait TimelineQueryDays // 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 - $query->select($fileid, 'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h', 'm.liveid') + $query->select($fileid, ...TimelineQuery::TIMELINE_SELECT) ->from('memories', 'm') ; // JOIN with filecache for existing files $query = $this->joinFilecache($query, $root, $recursive, $archive); - $query->addSelect('f.etag', 'f.path', 'f.name AS basename'); // SELECT rootid if not a single folder if ($recursive && !$root->isEmpty()) { @@ -168,7 +167,6 @@ trait TimelineQueryDays // JOIN with mimetypes to get the mimetype $query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id')); - $query->addSelect('mimetypes.mimetype'); // Filter by dayid unless wildcard if (null !== $day_ids) { diff --git a/lib/Db/TimelineQuerySingleItem.php b/lib/Db/TimelineQuerySingleItem.php new file mode 100644 index 00000000..cb6410e4 --- /dev/null +++ b/lib/Db/TimelineQuerySingleItem.php @@ -0,0 +1,32 @@ +connection->getQueryBuilder(); + $query->select('m.fileid', ...TimelineQuery::TIMELINE_SELECT) + ->from('memories', 'm') + ->where($query->expr()->eq('m.fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ; + + // JOIN filecache for etag + $query->innerJoin('m', 'filecache', 'f', $query->expr()->eq('f.fileid', 'm.fileid')); + + // JOIN with mimetypes to get the mimetype + $query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id')); + + unset($row['datetaken'], $row['path']); + + return $query->executeQuery()->fetch(); + } +} diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index ca8253cd..257e33fe 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -766,6 +766,8 @@ export default defineComponent({ data = await dav.getPlacesData(); } else if (this.$route.name === "tags" && !this.$route.params.name) { data = await dav.getTagsData(); + } else if (dav.isSingleItem()) { + data = await dav.getSingleItemData(); } else { // Try the cache try { diff --git a/src/services/DavRequests.ts b/src/services/DavRequests.ts index ff10e1e3..8f0de647 100644 --- a/src/services/DavRequests.ts +++ b/src/services/DavRequests.ts @@ -9,3 +9,4 @@ export * from "./dav/onthisday"; export * from "./dav/tags"; export * from "./dav/other"; export * from "./dav/places"; +export * from "./dav/single-item"; diff --git a/src/services/dav/single-item.ts b/src/services/dav/single-item.ts new file mode 100644 index 00000000..0351e1ea --- /dev/null +++ b/src/services/dav/single-item.ts @@ -0,0 +1,22 @@ +import { IDay } from "../../types"; +import { loadState } from "@nextcloud/initial-state"; + +const singleItem = JSON.parse(loadState("memories", "single_item", "{}")); + +export function isSingleItem(): boolean { + return Boolean(singleItem?.fileid); +} + +export async function getSingleItemData(): Promise { + if (!singleItem?.fileid) { + return []; + } + + return [ + { + dayid: singleItem.dayid, + count: 1, + detail: [singleItem], + }, + ] as any[]; +}