From dc2615e10713a764ef8aa6f13225229484eff518 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 25 Sep 2022 16:02:26 -0700 Subject: [PATCH] Implement archive (close #38) --- appinfo/routes.php | 2 + lib/Controller/ApiController.php | 117 +++++++++++++++++++++++++++++- lib/Controller/PageController.php | 8 ++ lib/Db/TimelineQueryDays.php | 26 ++++++- lib/Util.php | 2 + src/App.vue | 6 ++ src/components/Timeline.vue | 56 ++++++++++++++ src/router.ts | 9 +++ src/services/DavRequests.ts | 37 ++++++++++ 9 files changed, 258 insertions(+), 5 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index be5641a9..a78b0e13 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -13,12 +13,14 @@ return [ ], ['name' => 'page#favorites', 'url' => '/favorites', 'verb' => 'GET'], ['name' => 'page#videos', 'url' => '/videos', 'verb' => 'GET'], + ['name' => 'page#archive', 'url' => '/archive', 'verb' => 'GET'], // API ['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'], ['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'], ['name' => 'api#imageInfo', 'url' => '/api/info/{id}', 'verb' => 'GET'], ['name' => 'api#imageEdit', 'url' => '/api/edit/{id}', 'verb' => 'PATCH'], + ['name' => 'api#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'], // Config API ['name' => 'api#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'], diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 321e5edd..c260e88c 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -85,7 +85,7 @@ class ApiController extends Controller { } /** Preload a few "day" at the start of "days" response */ - private function preloadDays(array &$days, Folder &$folder, bool $recursive) { + private function preloadDays(array &$days, Folder &$folder, bool $recursive, bool $archive) { $uid = $this->userSession->getUser()->getUID(); $transforms = $this->getTransformations(); $preloaded = 0; @@ -95,6 +95,7 @@ class ApiController extends Controller { $uid, $day["dayid"], $recursive, + $archive, $transforms, ); $day["count"] = count($day["detail"]); // make sure count is accurate @@ -145,6 +146,7 @@ class ApiController extends Controller { // Get the folder to show $folder = $this->getRequestFolder(); $recursive = is_null($this->request->getParam('folder')); + $archive = !is_null($this->request->getParam('archive')); if (is_null($folder)) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } @@ -154,11 +156,12 @@ class ApiController extends Controller { $folder, $uid, $recursive, + $archive, $this->getTransformations(), ); // Preload some day responses - $this->preloadDays($list, $folder, $recursive); + $this->preloadDays($list, $folder, $recursive, $archive); // Add subfolder info if querying non-recursively if (!$recursive) { @@ -183,6 +186,7 @@ class ApiController extends Controller { // Get the folder to show $folder = $this->getRequestFolder(); $recursive = is_null($this->request->getParam('folder')); + $archive = !is_null($this->request->getParam('archive')); if (is_null($folder)) { return new JSONResponse([], Http::STATUS_NOT_FOUND); } @@ -193,6 +197,7 @@ class ApiController extends Controller { $uid, intval($id), $recursive, + $archive, $this->getTransformations(), ); return new JSONResponse($list, Http::STATUS_OK); @@ -312,6 +317,114 @@ class ApiController extends Controller { return $this->imageInfo($id); } + /** + * @NoAdminRequired + * + * Move one file to the archive folder + * @param string fileid + */ + public function archive(string $id): JSONResponse { + $user = $this->userSession->getUser(); + if (is_null($user)) { + return new JSONResponse(["message" => "Not logged in"], Http::STATUS_PRECONDITION_FAILED); + } + $uid = $user->getUID(); + $userFolder = $this->rootFolder->getUserFolder($uid); + + // Check for permissions and get numeric Id + $file = $userFolder->getById(intval($id)); + if (count($file) === 0) { + return new JSONResponse(["message" => "No such file"], Http::STATUS_NOT_FOUND); + } + $file = $file[0]; + + // Check if user has permissions + if (!$file->isUpdateable()) { + return new JSONResponse(["message" => "Cannot update this file"], Http::STATUS_FORBIDDEN); + } + + // Create archive folder in the root of the user's configured timeline + $timelinePath = Exif::removeExtraSlash(Exif::getPhotosPath($this->config, $uid)); + $timelineFolder = $userFolder->get($timelinePath); + if (is_null($timelineFolder) || !$timelineFolder instanceof Folder) { + return new JSONResponse(["message" => "Cannot get timeline"], Http::STATUS_INTERNAL_SERVER_ERROR); + } + if (!$timelineFolder->isCreatable()) { + return new JSONResponse(["message" => "Cannot create archive folder"], Http::STATUS_FORBIDDEN); + } + + // Get path of current file relative to the timeline folder + // remove timelineFolder path from start of file path + $timelinePath = $timelineFolder->getPath(); // no trailing slash + if (substr($file->getPath(), 0, strlen($timelinePath)) !== $timelinePath) { + return new JSONResponse(["message" => "Files outside timeline cannot be archived"], Http::STATUS_INTERNAL_SERVER_ERROR); + } + $relativePath = substr($file->getPath(), strlen($timelinePath)); // has a leading slash + + // Final path of the file including the file name + $destinationPath = ''; + + // Check if we want to archive or unarchive + $body = $this->request->getParams(); + $unarchive = isset($body['archive']) && $body['archive'] === false; + + // Get if the file is already in the archive (relativePath starts with archive) + $archiveFolderWithLeadingSlash = '/' . \OCA\Memories\Util::$ARCHIVE_FOLDER; + if (substr($relativePath, 0, strlen($archiveFolderWithLeadingSlash)) === $archiveFolderWithLeadingSlash) { + // file already in archive, remove it instead + $destinationPath = substr($relativePath, strlen($archiveFolderWithLeadingSlash)); + if (!$unarchive) { + return new JSONResponse(["message" => "File already archived"], Http::STATUS_BAD_REQUEST); + } + } else { + // file not in archive, put it in there + $destinationPath = Exif::removeExtraSlash(\OCA\Memories\Util::$ARCHIVE_FOLDER . $relativePath); + if ($unarchive) { + return new JSONResponse(["message" => "File not archived"], Http::STATUS_BAD_REQUEST); + } + } + + // Remove the filename + $destinationFolders = explode('/', $destinationPath); + array_pop($destinationFolders); + + // Create folder tree + $folder = $timelineFolder; + foreach ($destinationFolders as $folderName) { + if ($folderName === '') { + continue; + } + try { + $existingFolder = $folder->get($folderName . '/'); + if (!$existingFolder instanceof Folder) { + throw new \OCP\Files\NotFoundException('Not a folder'); + } + $folder = $existingFolder; + } catch (\OCP\Files\NotFoundException $e) { + try { + $folder = $folder->newFolder($folderName); + } catch (\OCP\Files\NotPermittedException $e) { + return new JSONResponse(["message" => "Failed to create folder"], Http::STATUS_FORBIDDEN); + } + } + } + + // Move file to archive folder + try { + $file->move($folder->getPath() . '/' . $file->getName()); + } catch (\OCP\Files\NotPermittedException $e) { + return new JSONResponse(["message" => "Failed to move file"], Http::STATUS_FORBIDDEN); + } catch (\OCP\Files\NotFoundException $e) { + return new JSONResponse(["message" => "File not found"], Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (\OCP\Files\InvalidPathException $e) { + return new JSONResponse(["message" => "Invalid path"], Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (\OCP\Lock\LockedException $e) { + return new JSONResponse(["message" => "File is locked"], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + return new JSONResponse([], Http::STATUS_OK); + } + /** * @NoAdminRequired * diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index d9ff8e11..5ade7ec2 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -88,4 +88,12 @@ class PageController extends Controller { public function videos() { return $this->main(); } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function archive() { + return $this->main(); + } } diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index 01974df9..70a6f725 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -46,7 +46,12 @@ trait TimelineQueryDays { } /** Get the query for oc_filecache join */ - private function getFilecacheJoinQuery(IQueryBuilder &$query, Folder &$folder, bool $recursive) { + private function getFilecacheJoinQuery( + IQueryBuilder &$query, + Folder &$folder, + bool $recursive, + bool $archive + ) { // Subquery to get storage and path $subQuery = $query->getConnection()->getQueryBuilder(); $cursor = $subQuery->select('path', 'storage')->from('filecache')->where( @@ -67,6 +72,19 @@ trait TimelineQueryDays { } $likePath = $likePath . '%'; $pathQuery = $query->expr()->like('f.path', $query->createNamedParameter($likePath)); + + // Exclude/show archive folder + $archiveLikePath = $finfo["path"] . '/' . \OCA\Memories\Util::$ARCHIVE_FOLDER . '/%'; + if (!$archive) { + // Exclude archive folder + $pathQuery = $query->expr()->andX( + $pathQuery, + $query->expr()->notLike('f.path', $query->createNamedParameter($archiveLikePath)) + ); + } else { + // Show only archive folder + $pathQuery = $query->expr()->like('f.path', $query->createNamedParameter($archiveLikePath)); + } } else { // If getting non-recursively folder only check for parent $pathQuery = $query->expr()->eq('f.parent', $query->createNamedParameter($folder->getId(), IQueryBuilder::PARAM_INT)); @@ -87,6 +105,7 @@ trait TimelineQueryDays { Folder &$folder, string $uid, bool $recursive, + bool $archive, array $queryTransforms = [] ): array { $query = $this->connection->getQueryBuilder(); @@ -95,7 +114,7 @@ trait TimelineQueryDays { $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); $query->select('m.dayid', $count) ->from('memories', 'm') - ->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive)); + ->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive, $archive)); // Group and sort by dayid $query->groupBy('m.dayid') @@ -121,6 +140,7 @@ trait TimelineQueryDays { string $uid, int $dayid, bool $recursive, + bool $archive, array $queryTransforms = [] ): array { $query = $this->connection->getQueryBuilder(); @@ -133,7 +153,7 @@ trait TimelineQueryDays { // when using DISTINCT on selected fields $query->select($fileid, 'f.etag', 'm.isvideo', 'vco.categoryid', 'm.datetaken') ->from('memories', 'm') - ->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive)) + ->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive, $archive)) ->andWhere($query->expr()->eq('m.dayid', $query->createNamedParameter($dayid, IQueryBuilder::PARAM_INT))); // Add favorite field diff --git a/lib/Util.php b/lib/Util.php index 35b9be51..414d40be 100644 --- a/lib/Util.php +++ b/lib/Util.php @@ -10,6 +10,8 @@ class Util { public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist public static $TAG_DAYID_FOLDERS = -(1 << 30) + 1; + public static $ARCHIVE_FOLDER = '.archive'; + /** * Get the path to the user's configured photos directory. * @param IConfig $config diff --git a/src/App.vue b/src/App.vue index 4c44e891..15932ae2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,6 +19,10 @@ :title="t('memories', 'Videos')">