2022-08-13 03:34:05 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
|
|
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
|
|
|
*
|
|
|
|
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
|
|
|
*
|
|
|
|
* @license AGPL-3.0-or-later
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as
|
|
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2022-08-18 18:27:25 +00:00
|
|
|
namespace OCA\Memories\Controller;
|
2022-08-13 03:34:05 +00:00
|
|
|
|
2022-08-18 18:27:25 +00:00
|
|
|
use OCA\Memories\AppInfo\Application;
|
2022-08-20 02:53:21 +00:00
|
|
|
use OCA\Memories\Db\TimelineQuery;
|
2022-09-25 13:21:40 +00:00
|
|
|
use OCA\Memories\Db\TimelineWrite;
|
2022-09-24 01:54:14 +00:00
|
|
|
use OCA\Memories\Exif;
|
2022-10-07 20:21:46 +00:00
|
|
|
use OCP\App\IAppManager;
|
2022-08-13 03:34:05 +00:00
|
|
|
use OCP\AppFramework\Controller;
|
|
|
|
use OCP\AppFramework\Http;
|
|
|
|
use OCP\AppFramework\Http\JSONResponse;
|
2022-10-17 02:52:44 +00:00
|
|
|
use OCP\AppFramework\Http\StreamResponse;
|
|
|
|
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
2022-10-17 17:41:58 +00:00
|
|
|
use OCP\AppFramework\Http\DataResponse;
|
|
|
|
use OCP\AppFramework\Http\DataDisplayResponse;
|
2022-08-17 20:39:48 +00:00
|
|
|
use OCP\Files\IRootFolder;
|
2022-10-17 17:41:58 +00:00
|
|
|
use OCP\Files\FileInfo;
|
|
|
|
use OCP\Files\Folder;
|
2022-08-13 03:34:05 +00:00
|
|
|
use OCP\IConfig;
|
|
|
|
use OCP\IDBConnection;
|
|
|
|
use OCP\IRequest;
|
|
|
|
use OCP\IUserSession;
|
2022-10-17 17:41:58 +00:00
|
|
|
use OCP\IPreview;
|
2022-08-13 03:34:05 +00:00
|
|
|
|
|
|
|
class ApiController extends Controller {
|
2022-09-09 07:31:42 +00:00
|
|
|
private IConfig $config;
|
|
|
|
private IUserSession $userSession;
|
2022-08-13 03:34:05 +00:00
|
|
|
private IDBConnection $connection;
|
2022-09-09 07:31:42 +00:00
|
|
|
private IRootFolder $rootFolder;
|
2022-10-07 20:21:46 +00:00
|
|
|
private IAppManager $appManager;
|
2022-09-09 07:31:42 +00:00
|
|
|
private TimelineQuery $timelineQuery;
|
2022-09-25 13:21:40 +00:00
|
|
|
private TimelineWrite $timelineWrite;
|
2022-10-17 17:41:58 +00:00
|
|
|
private IPreview $previewManager;
|
2022-08-13 03:34:05 +00:00
|
|
|
|
2022-09-09 07:31:42 +00:00
|
|
|
public function __construct(
|
|
|
|
IRequest $request,
|
|
|
|
IConfig $config,
|
|
|
|
IUserSession $userSession,
|
2022-08-17 20:39:48 +00:00
|
|
|
IDBConnection $connection,
|
2022-10-07 20:21:46 +00:00
|
|
|
IRootFolder $rootFolder,
|
2022-10-17 17:41:58 +00:00
|
|
|
IAppManager $appManager,
|
|
|
|
IPreview $previewManager) {
|
2022-08-22 18:48:35 +00:00
|
|
|
|
2022-09-09 07:31:42 +00:00
|
|
|
parent::__construct(Application::APPNAME, $request);
|
2022-08-13 03:34:05 +00:00
|
|
|
|
2022-09-09 07:31:42 +00:00
|
|
|
$this->config = $config;
|
|
|
|
$this->userSession = $userSession;
|
2022-08-13 03:34:05 +00:00
|
|
|
$this->connection = $connection;
|
2022-09-09 07:31:42 +00:00
|
|
|
$this->rootFolder = $rootFolder;
|
2022-10-07 20:21:46 +00:00
|
|
|
$this->appManager = $appManager;
|
2022-10-17 17:41:58 +00:00
|
|
|
$this->previewManager = $previewManager;
|
2022-09-12 01:33:38 +00:00
|
|
|
$this->timelineQuery = new TimelineQuery($this->connection);
|
2022-09-25 13:21:40 +00:00
|
|
|
$this->timelineWrite = new TimelineWrite($connection);
|
2022-09-12 01:33:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get transformations depending on the request
|
|
|
|
*/
|
|
|
|
private function getTransformations() {
|
|
|
|
$transforms = array();
|
|
|
|
|
|
|
|
// Filter only favorites
|
|
|
|
if ($this->request->getParam('fav')) {
|
|
|
|
$transforms[] = array($this->timelineQuery, 'transformFavoriteFilter');
|
|
|
|
}
|
|
|
|
|
2022-09-13 07:55:32 +00:00
|
|
|
// Filter only videos
|
|
|
|
if ($this->request->getParam('vid')) {
|
2022-10-06 21:19:47 +00:00
|
|
|
$transforms[] = array($this->timelineQuery, 'transformVideoFilter');
|
|
|
|
}
|
|
|
|
|
2022-10-07 19:28:39 +00:00
|
|
|
// Filter only for one face
|
2022-10-07 20:21:46 +00:00
|
|
|
if ($this->recognizeIsEnabled()) {
|
2022-10-08 06:26:09 +00:00
|
|
|
$face = $this->request->getParam('face');
|
|
|
|
if ($face) {
|
|
|
|
$transforms[] = array($this->timelineQuery, 'transformFaceFilter', $face);
|
2022-10-07 20:21:46 +00:00
|
|
|
}
|
2022-10-18 21:08:27 +00:00
|
|
|
|
|
|
|
$faceRect = $this->request->getParam('facerect');
|
|
|
|
if ($faceRect) {
|
|
|
|
$transforms[] = array($this->timelineQuery, 'transformFaceRect', $face);
|
|
|
|
}
|
2022-10-07 19:28:39 +00:00
|
|
|
}
|
|
|
|
|
2022-10-06 21:19:47 +00:00
|
|
|
// Filter only for one tag
|
2022-10-07 20:21:46 +00:00
|
|
|
if ($this->tagsIsEnabled()) {
|
|
|
|
$tagName = $this->request->getParam('tag');
|
|
|
|
if ($tagName) {
|
|
|
|
$transforms[] = array($this->timelineQuery, 'transformTagFilter', $tagName);
|
|
|
|
}
|
2022-09-13 07:55:32 +00:00
|
|
|
}
|
|
|
|
|
2022-10-06 21:44:14 +00:00
|
|
|
// Limit number of responses for day query
|
|
|
|
$limit = $this->request->getParam('limit');
|
|
|
|
if ($limit) {
|
|
|
|
$transforms[] = array($this->timelineQuery, 'transformLimitDay', intval($limit));
|
|
|
|
}
|
|
|
|
|
2022-09-12 01:33:38 +00:00
|
|
|
return $transforms;
|
2022-09-09 07:31:42 +00:00
|
|
|
}
|
|
|
|
|
2022-09-13 03:21:25 +00:00
|
|
|
/** Preload a few "day" at the start of "days" response */
|
2022-09-25 23:02:26 +00:00
|
|
|
private function preloadDays(array &$days, Folder &$folder, bool $recursive, bool $archive) {
|
2022-09-13 03:21:25 +00:00
|
|
|
$uid = $this->userSession->getUser()->getUID();
|
|
|
|
$transforms = $this->getTransformations();
|
|
|
|
$preloaded = 0;
|
|
|
|
foreach ($days as &$day) {
|
|
|
|
$day["detail"] = $this->timelineQuery->getDay(
|
2022-09-24 01:54:14 +00:00
|
|
|
$folder,
|
2022-09-13 03:21:25 +00:00
|
|
|
$uid,
|
2022-10-06 19:24:45 +00:00
|
|
|
[$day["dayid"]],
|
2022-09-24 01:54:14 +00:00
|
|
|
$recursive,
|
2022-09-25 23:02:26 +00:00
|
|
|
$archive,
|
2022-09-13 03:21:25 +00:00
|
|
|
$transforms,
|
|
|
|
);
|
|
|
|
$day["count"] = count($day["detail"]); // make sure count is accurate
|
|
|
|
$preloaded += $day["count"];
|
|
|
|
|
|
|
|
if ($preloaded >= 50) { // should be enough
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-24 01:54:14 +00:00
|
|
|
/** Get the Folder object relevant to the request */
|
|
|
|
private function getRequestFolder() {
|
|
|
|
$uid = $this->userSession->getUser()->getUID();
|
|
|
|
try {
|
|
|
|
$folder = null;
|
|
|
|
$folderPath = $this->request->getParam('folder');
|
|
|
|
$userFolder = $this->rootFolder->getUserFolder($uid);
|
|
|
|
|
|
|
|
if (!is_null($folderPath)) {
|
|
|
|
$folder = $userFolder->get($folderPath);
|
|
|
|
} else {
|
|
|
|
$configPath = Exif::removeExtraSlash(Exif::getPhotosPath($this->config, $uid));
|
|
|
|
$folder = $userFolder->get($configPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$folder instanceof Folder) {
|
|
|
|
throw new \Exception("Folder not found");
|
|
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return $folder;
|
|
|
|
}
|
|
|
|
|
2022-09-09 07:31:42 +00:00
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
*
|
|
|
|
* @return JSONResponse
|
|
|
|
*/
|
|
|
|
public function days(): JSONResponse {
|
2022-08-13 03:34:05 +00:00
|
|
|
$user = $this->userSession->getUser();
|
2022-09-09 07:31:42 +00:00
|
|
|
if (is_null($user)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
2022-09-24 01:54:14 +00:00
|
|
|
$uid = $user->getUID();
|
2022-08-13 03:34:05 +00:00
|
|
|
|
2022-09-24 01:54:14 +00:00
|
|
|
// Get the folder to show
|
|
|
|
$folder = $this->getRequestFolder();
|
|
|
|
$recursive = is_null($this->request->getParam('folder'));
|
2022-09-25 23:02:26 +00:00
|
|
|
$archive = !is_null($this->request->getParam('archive'));
|
2022-09-24 01:54:14 +00:00
|
|
|
if (is_null($folder)) {
|
2022-10-08 06:36:16 +00:00
|
|
|
return new JSONResponse(["message" => "Folder not found"], Http::STATUS_NOT_FOUND);
|
2022-09-24 01:54:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run actual query
|
2022-10-08 06:46:08 +00:00
|
|
|
try {
|
|
|
|
$list = $this->timelineQuery->getDays(
|
|
|
|
$folder,
|
|
|
|
$uid,
|
|
|
|
$recursive,
|
|
|
|
$archive,
|
|
|
|
$this->getTransformations(),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Preload some day responses
|
|
|
|
$this->preloadDays($list, $folder, $recursive, $archive);
|
|
|
|
|
|
|
|
// Add subfolder info if querying non-recursively
|
|
|
|
if (!$recursive) {
|
|
|
|
array_unshift($list, $this->getSubfoldersEntry($folder));
|
|
|
|
}
|
2022-09-24 01:54:14 +00:00
|
|
|
|
2022-10-08 06:46:08 +00:00
|
|
|
return new JSONResponse($list, Http::STATUS_OK);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
|
|
|
}
|
2022-09-09 07:31:42 +00:00
|
|
|
}
|
|
|
|
|
2022-10-06 20:37:12 +00:00
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
*
|
|
|
|
* @return JSONResponse
|
|
|
|
*/
|
|
|
|
public function dayPost(): JSONResponse {
|
|
|
|
$id = $this->request->getParam('body_ids');
|
|
|
|
if (is_null($id)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
|
|
|
|
}
|
|
|
|
return $this->day($id);
|
|
|
|
}
|
|
|
|
|
2022-09-09 07:31:42 +00:00
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
*
|
|
|
|
* @return JSONResponse
|
|
|
|
*/
|
|
|
|
public function day(string $id): JSONResponse {
|
2022-08-14 23:31:47 +00:00
|
|
|
$user = $this->userSession->getUser();
|
2022-09-24 01:54:14 +00:00
|
|
|
if (is_null($user)) {
|
2022-09-09 07:31:42 +00:00
|
|
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
2022-09-24 01:54:14 +00:00
|
|
|
$uid = $user->getUID();
|
|
|
|
|
2022-10-06 22:01:28 +00:00
|
|
|
// Check for wildcard
|
|
|
|
$day_ids = [];
|
|
|
|
if ($id === "*") {
|
|
|
|
$day_ids = null;
|
|
|
|
} else {
|
|
|
|
// Split at commas and convert all parts to int
|
|
|
|
$day_ids = array_map(function ($part) {
|
|
|
|
return intval($part);
|
|
|
|
}, explode(",", $id));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if $day_ids is empty
|
|
|
|
if (!is_null($day_ids) && count($day_ids) === 0) {
|
|
|
|
return new JSONResponse([], Http::STATUS_OK);
|
|
|
|
}
|
2022-10-06 19:24:45 +00:00
|
|
|
|
2022-09-24 01:54:14 +00:00
|
|
|
// Get the folder to show
|
|
|
|
$folder = $this->getRequestFolder();
|
|
|
|
$recursive = is_null($this->request->getParam('folder'));
|
2022-09-25 23:02:26 +00:00
|
|
|
$archive = !is_null($this->request->getParam('archive'));
|
2022-09-24 01:54:14 +00:00
|
|
|
if (is_null($folder)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
|
|
|
}
|
2022-08-14 23:31:47 +00:00
|
|
|
|
2022-09-24 01:54:14 +00:00
|
|
|
// Run actual query
|
2022-10-08 06:46:08 +00:00
|
|
|
try {
|
|
|
|
$list = $this->timelineQuery->getDay(
|
|
|
|
$folder,
|
|
|
|
$uid,
|
|
|
|
$day_ids,
|
|
|
|
$recursive,
|
|
|
|
$archive,
|
|
|
|
$this->getTransformations(),
|
|
|
|
);
|
|
|
|
return new JSONResponse($list, Http::STATUS_OK);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
|
|
|
}
|
2022-09-09 07:31:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-09-25 09:46:01 +00:00
|
|
|
* Get subfolders entry for days response
|
2022-09-09 07:31:42 +00:00
|
|
|
*/
|
2022-09-25 09:46:01 +00:00
|
|
|
public function getSubfoldersEntry(Folder &$folder) {
|
|
|
|
// Ugly: get the view of the folder with reflection
|
|
|
|
// This is unfortunately the only way to get the contents of a folder
|
|
|
|
// matching a MIME type without using SEARCH, which is deep
|
|
|
|
$rp = new \ReflectionProperty('\OC\Files\Node\Node', 'view');
|
|
|
|
$rp->setAccessible(true);
|
|
|
|
$view = $rp->getValue($folder);
|
|
|
|
|
|
|
|
// Get the subfolders
|
|
|
|
$folders = $view->getDirectoryContent($folder->getPath(), FileInfo::MIMETYPE_FOLDER, $folder);
|
2022-09-09 07:31:42 +00:00
|
|
|
|
|
|
|
// Sort by name
|
2022-09-25 09:46:01 +00:00
|
|
|
usort($folders, function($a, $b) {
|
2022-09-09 07:31:42 +00:00
|
|
|
return strnatcmp($a->getName(), $b->getName());
|
|
|
|
});
|
|
|
|
|
2022-09-25 09:46:01 +00:00
|
|
|
// Process to response type
|
|
|
|
return [
|
2022-09-16 21:37:52 +00:00
|
|
|
"dayid" => \OCA\Memories\Util::$TAG_DAYID_FOLDERS,
|
2022-09-25 09:46:01 +00:00
|
|
|
"count" => count($folders),
|
2022-09-25 10:00:07 +00:00
|
|
|
"detail" => array_map(function ($node) {
|
2022-09-09 07:31:42 +00:00
|
|
|
return [
|
|
|
|
"fileid" => $node->getId(),
|
|
|
|
"name" => $node->getName(),
|
2022-09-13 04:59:35 +00:00
|
|
|
"isfolder" => 1,
|
2022-09-09 07:31:42 +00:00
|
|
|
"path" => $node->getPath(),
|
|
|
|
];
|
2022-09-25 09:46:01 +00:00
|
|
|
}, $folders, []),
|
2022-09-09 07:31:42 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-10-06 21:37:18 +00:00
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
*
|
|
|
|
* Get list of tags with counts of images
|
|
|
|
* @return JSONResponse
|
|
|
|
*/
|
|
|
|
public function tags(): JSONResponse {
|
|
|
|
$user = $this->userSession->getUser();
|
|
|
|
if (is_null($user)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
|
|
|
|
2022-10-07 20:21:46 +00:00
|
|
|
// Check tags enabled for this user
|
|
|
|
if (!$this->tagsIsEnabled()) {
|
|
|
|
return new JSONResponse(["message" => "Tags not enabled for user"], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
|
|
|
|
2022-10-06 21:37:18 +00:00
|
|
|
// If this isn't the timeline folder then things aren't going to work
|
|
|
|
$folder = $this->getRequestFolder();
|
|
|
|
if (is_null($folder)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run actual query
|
|
|
|
$list = $this->timelineQuery->getTags(
|
|
|
|
$folder,
|
|
|
|
);
|
2022-10-08 00:57:48 +00:00
|
|
|
|
|
|
|
// Preload all tag previews
|
2022-10-08 02:09:05 +00:00
|
|
|
$previews = $this->timelineQuery->getTagPreviews($folder);
|
|
|
|
|
|
|
|
// Convert to map with key as systemtagid
|
|
|
|
$previews_map = [];
|
|
|
|
foreach ($previews as &$preview) {
|
|
|
|
$key = $preview["systemtagid"];
|
|
|
|
if (!array_key_exists($key, $previews_map)) {
|
|
|
|
$previews_map[$key] = [];
|
|
|
|
}
|
|
|
|
unset($preview["systemtagid"]);
|
|
|
|
$previews_map[$key][] = $preview;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add previews to list
|
2022-10-08 00:57:48 +00:00
|
|
|
foreach ($list as &$tag) {
|
2022-10-08 02:09:05 +00:00
|
|
|
$key = $tag["id"];
|
|
|
|
if (array_key_exists($key, $previews_map)) {
|
|
|
|
$tag["previews"] = $previews_map[$key];
|
|
|
|
} else {
|
|
|
|
$tag["previews"] = [];
|
|
|
|
}
|
2022-10-08 00:57:48 +00:00
|
|
|
}
|
|
|
|
|
2022-10-06 21:37:18 +00:00
|
|
|
return new JSONResponse($list, Http::STATUS_OK);
|
|
|
|
}
|
|
|
|
|
2022-10-07 19:28:39 +00:00
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
*
|
|
|
|
* Get list of faces with counts of images
|
|
|
|
* @return JSONResponse
|
|
|
|
*/
|
|
|
|
public function faces(): JSONResponse {
|
|
|
|
$user = $this->userSession->getUser();
|
|
|
|
if (is_null($user)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
|
|
|
|
2022-10-07 20:21:46 +00:00
|
|
|
// Check faces enabled for this user
|
|
|
|
if (!$this->recognizeIsEnabled()) {
|
|
|
|
return new JSONResponse(["message" => "Recognize app not enabled or not v3+"], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
|
|
|
|
2022-10-07 19:28:39 +00:00
|
|
|
// If this isn't the timeline folder then things aren't going to work
|
|
|
|
$folder = $this->getRequestFolder();
|
|
|
|
if (is_null($folder)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run actual query
|
|
|
|
$list = $this->timelineQuery->getFaces(
|
|
|
|
$folder,
|
|
|
|
);
|
2022-10-08 00:57:48 +00:00
|
|
|
|
2022-10-17 17:41:58 +00:00
|
|
|
return new JSONResponse($list, Http::STATUS_OK);
|
|
|
|
}
|
2022-10-08 02:00:55 +00:00
|
|
|
|
2022-10-17 17:41:58 +00:00
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
* @NoCSRFRequired
|
|
|
|
*
|
|
|
|
* Get face preview image cropped with imagick
|
|
|
|
* @return DataResponse
|
|
|
|
*/
|
|
|
|
public function facePreview(string $id): Http\Response {
|
|
|
|
$user = $this->userSession->getUser();
|
|
|
|
if (is_null($user)) {
|
|
|
|
return new DataResponse([], Http::STATUS_PRECONDITION_FAILED);
|
2022-10-07 20:21:46 +00:00
|
|
|
}
|
|
|
|
|
2022-10-17 17:41:58 +00:00
|
|
|
// Check faces enabled for this user
|
|
|
|
if (!$this->recognizeIsEnabled()) {
|
|
|
|
return new DataResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get folder to search for
|
|
|
|
$folder = $this->getRequestFolder();
|
|
|
|
if (is_null($folder)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run actual query
|
|
|
|
$detections = $this->timelineQuery->getFacePreviewDetection($folder, intval($id));
|
|
|
|
if (is_null($detections) || count($detections) == 0) {
|
|
|
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the first detection that has a preview
|
|
|
|
$preview = null;
|
|
|
|
foreach ($detections as &$detection) {
|
|
|
|
// Get the file (also checks permissions)
|
|
|
|
$files = $folder->getById($detection["file_id"]);
|
|
|
|
if (count($files) == 0 || $files[0]->getType() != FileInfo::TYPE_FILE) {
|
|
|
|
continue;
|
2022-10-08 02:00:55 +00:00
|
|
|
}
|
2022-10-17 17:41:58 +00:00
|
|
|
|
|
|
|
// Get (hopefully cached) preview image
|
|
|
|
try {
|
|
|
|
$preview = $this->previewManager->getPreview($files[0], 2048, 2048, false);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Got the preview
|
|
|
|
break;
|
2022-10-07 19:28:39 +00:00
|
|
|
}
|
|
|
|
|
2022-10-17 17:41:58 +00:00
|
|
|
// Make sure the preview is valid
|
|
|
|
if (is_null($preview)) {
|
|
|
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Crop image
|
|
|
|
$image = new \Imagick();
|
|
|
|
$image->readImageBlob($preview->getContent());
|
|
|
|
$iw = $image->getImageWidth();
|
|
|
|
$ih = $image->getImageHeight();
|
|
|
|
$dw = floatval($detection["width"]);
|
|
|
|
$dh = floatval($detection["height"]);
|
|
|
|
$dcx = floatval($detection["x"]) + floatval($detection["width"]) / 2;
|
|
|
|
$dcy = floatval($detection["y"]) + floatval($detection["height"]) / 2;
|
|
|
|
$faceDim = max($dw * $iw, $dh * $ih) * 1.5;
|
|
|
|
$image->cropImage(
|
|
|
|
intval($faceDim),
|
|
|
|
intval($faceDim),
|
|
|
|
intval($dcx * $iw - $faceDim / 2),
|
|
|
|
intval($dcy * $ih - $faceDim / 2),
|
|
|
|
);
|
2022-10-18 14:54:44 +00:00
|
|
|
$image->scaleImage(256, 256, true);
|
2022-10-17 17:41:58 +00:00
|
|
|
$blob = $image->getImageBlob();
|
|
|
|
|
|
|
|
// Create and send response
|
|
|
|
$response = new DataDisplayResponse($blob, Http::STATUS_OK, [
|
|
|
|
'Content-Type' => $image->getImageMimeType(),
|
|
|
|
]);
|
|
|
|
$response->cacheFor(3600 * 24, false, false);
|
|
|
|
return $response;
|
2022-10-07 19:28:39 +00:00
|
|
|
}
|
|
|
|
|
2022-09-25 13:21:40 +00:00
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
*
|
|
|
|
* Get image info for one file
|
|
|
|
* @param string fileid
|
|
|
|
*/
|
|
|
|
public function imageInfo(string $id): JSONResponse {
|
|
|
|
$user = $this->userSession->getUser();
|
|
|
|
if (is_null($user)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
|
|
|
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
|
|
|
|
|
|
|
|
// Check for permissions and get numeric Id
|
|
|
|
$file = $userFolder->getById(intval($id));
|
|
|
|
if (count($file) === 0) {
|
|
|
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
|
|
|
}
|
|
|
|
$file = $file[0];
|
|
|
|
|
|
|
|
// Get the image info
|
|
|
|
$info = $this->timelineQuery->getInfoById($file->getId());
|
|
|
|
|
|
|
|
return new JSONResponse($info, Http::STATUS_OK);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
*
|
|
|
|
* Change exif data for one file
|
|
|
|
* @param string fileid
|
|
|
|
*/
|
|
|
|
public function imageEdit(string $id): JSONResponse {
|
|
|
|
$user = $this->userSession->getUser();
|
|
|
|
if (is_null($user)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
|
|
|
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
|
|
|
|
|
|
|
|
// Check for permissions and get numeric Id
|
|
|
|
$file = $userFolder->getById(intval($id));
|
|
|
|
if (count($file) === 0) {
|
|
|
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
|
|
|
}
|
|
|
|
$file = $file[0];
|
|
|
|
|
2022-09-25 15:09:04 +00:00
|
|
|
// Check if user has permissions
|
|
|
|
if (!$file->isUpdateable()) {
|
|
|
|
return new JSONResponse([], Http::STATUS_FORBIDDEN);
|
|
|
|
}
|
2022-09-25 13:21:40 +00:00
|
|
|
|
|
|
|
// Get new date from body
|
|
|
|
$body = $this->request->getParams();
|
|
|
|
if (!isset($body['date'])) {
|
|
|
|
return new JSONResponse(["message" => "Missing date"], Http::STATUS_BAD_REQUEST);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure the date is valid
|
|
|
|
try {
|
|
|
|
Exif::parseExifDate($body['date']);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_BAD_REQUEST);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update date
|
|
|
|
try {
|
|
|
|
$res = Exif::updateExifDate($file, $body['date']);
|
|
|
|
if ($res === false) {
|
|
|
|
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
|
|
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reprocess the file
|
|
|
|
$this->timelineWrite->processFile($file, true);
|
|
|
|
|
|
|
|
return $this->imageInfo($id);
|
|
|
|
}
|
|
|
|
|
2022-09-25 23:02:26 +00:00
|
|
|
/**
|
|
|
|
* @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);
|
|
|
|
}
|
|
|
|
|
2022-10-07 20:21:46 +00:00
|
|
|
/**
|
|
|
|
* Check if tags is enabled for this user
|
|
|
|
*/
|
|
|
|
private function tagsIsEnabled(): bool {
|
|
|
|
return $this->appManager->isEnabledForUser('systemtags');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if recognize is enabled for this user
|
|
|
|
*/
|
|
|
|
private function recognizeIsEnabled(): bool {
|
|
|
|
if (!$this->appManager->isEnabledForUser('recognize')) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$v = $this->appManager->getAppInfo('recognize')["version"];
|
|
|
|
return version_compare($v, "3.0.0-alpha", ">=");
|
|
|
|
}
|
|
|
|
|
2022-09-09 07:31:42 +00:00
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
*
|
|
|
|
* update preferences (user setting)
|
|
|
|
*
|
|
|
|
* @param string key the identifier to change
|
|
|
|
* @param string value the value to set
|
|
|
|
*
|
|
|
|
* @return JSONResponse an empty JSONResponse with respective http status code
|
|
|
|
*/
|
|
|
|
public function setUserConfig(string $key, string $value): JSONResponse {
|
|
|
|
$user = $this->userSession->getUser();
|
|
|
|
if (is_null($user)) {
|
|
|
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
|
|
}
|
|
|
|
|
|
|
|
$userId = $user->getUid();
|
|
|
|
$this->config->setUserValue($userId, Application::APPNAME, $key, $value);
|
|
|
|
return new JSONResponse([], Http::STATUS_OK);
|
|
|
|
}
|
2022-10-17 02:52:44 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @NoAdminRequired
|
|
|
|
* @NoCSRFRequired
|
|
|
|
*/
|
|
|
|
public function serviceWorker(): StreamResponse {
|
|
|
|
$response = new StreamResponse(__DIR__.'/../../js/memories-service-worker.js');
|
|
|
|
$response->setHeaders([
|
|
|
|
'Content-Type' => 'application/javascript',
|
|
|
|
'Service-Worker-Allowed' => '/'
|
|
|
|
]);
|
|
|
|
$policy = new ContentSecurityPolicy();
|
|
|
|
$policy->addAllowedWorkerSrcDomain("'self'");
|
|
|
|
$policy->addAllowedScriptDomain("'self'");
|
|
|
|
$policy->addAllowedConnectDomain("'self'");
|
|
|
|
$response->setContentSecurityPolicy($policy);
|
|
|
|
return $response;
|
|
|
|
}
|
2022-08-13 03:34:05 +00:00
|
|
|
}
|