Lint
parent
37a725ce1b
commit
26cb158b2e
|
@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022, Varun Patil <radialapps@gmail.com>
|
||||
*
|
||||
* @author Varun Patil <radialapps@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,22 +19,22 @@ declare(strict_types=1);
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\AppInfo;
|
||||
|
||||
use OCA\Memories\Listeners\PostWriteListener;
|
||||
use OCA\Memories\Listeners\PostDeleteListener;
|
||||
use OCA\Memories\Listeners\PostWriteListener;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\Files\Events\Node\NodeWrittenEvent;
|
||||
use OCP\Files\Events\Node\NodeDeletedEvent;
|
||||
use OCP\Files\Events\Node\NodeTouchedEvent;
|
||||
use OCP\Files\Events\Node\NodeWrittenEvent;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
class Application extends App implements IBootstrap
|
||||
{
|
||||
public const APPNAME = 'memories';
|
||||
|
||||
public const IMAGE_MIMES = [
|
||||
|
@ -61,16 +59,19 @@ class Application extends App implements IBootstrap {
|
|||
'video/x-matroska',
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::APPNAME);
|
||||
}
|
||||
|
||||
public function register(IRegistrationContext $context): void {
|
||||
public function register(IRegistrationContext $context): void
|
||||
{
|
||||
$context->registerEventListener(NodeWrittenEvent::class, PostWriteListener::class);
|
||||
$context->registerEventListener(NodeTouchedEvent::class, PostWriteListener::class);
|
||||
$context->registerEventListener(NodeDeletedEvent::class, PostDeleteListener::class);
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
public function boot(IBootContext $context): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022, Varun Patil <radialapps@gmail.com>
|
||||
*
|
||||
* @author Varun Patil <radialapps@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,11 +19,12 @@ declare(strict_types=1);
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Command;
|
||||
|
||||
use OCA\Files_External\Service\GlobalStoragesService;
|
||||
use OCA\Memories\Db\TimelineWrite;
|
||||
use OCP\Encryption\IManager;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\Folder;
|
||||
|
@ -36,8 +35,6 @@ use OCP\IDBConnection;
|
|||
use OCP\IPreview;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCA\Files_External\Service\GlobalStoragesService;
|
||||
use OCA\Memories\Db\TimelineWrite;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
@ -45,8 +42,8 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Index extends Command {
|
||||
|
||||
class Index extends Command
|
||||
{
|
||||
/** @var ?GlobalStoragesService */
|
||||
protected $globalService;
|
||||
|
||||
|
@ -67,13 +64,16 @@ class Index extends Command {
|
|||
private int $nSkipped = 0;
|
||||
private int $nInvalid = 0;
|
||||
|
||||
public function __construct(IRootFolder $rootFolder,
|
||||
public function __construct(
|
||||
IRootFolder $rootFolder,
|
||||
IUserManager $userManager,
|
||||
IPreview $previewGenerator,
|
||||
IConfig $config,
|
||||
IManager $encryptionManager,
|
||||
IDBConnection $connection,
|
||||
ContainerInterface $container) {
|
||||
ContainerInterface $container
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->userManager = $userManager;
|
||||
|
@ -91,38 +91,8 @@ class Index extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
/** Make sure exiftool is available */
|
||||
private function testExif() {
|
||||
$testfile = dirname(__FILE__). '/../../exiftest.jpg';
|
||||
$stream = fopen($testfile, 'rb');
|
||||
if (!$stream) {
|
||||
error_log("Couldn't open Exif test file $testfile");
|
||||
return false;
|
||||
}
|
||||
|
||||
$exif = null;
|
||||
try {
|
||||
$exif = \OCA\Memories\Exif::getExifFromStream($stream);
|
||||
} catch (\Exception $e) {
|
||||
error_log("Couldn't read Exif data from test file: " . $e->getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
if (!$exif) {
|
||||
error_log("Got blank Exif data from test file");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($exif["DateTimeOriginal"] !== "2004:08:31 19:52:58") {
|
||||
error_log("Got unexpected Exif data from test file");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('memories:index')
|
||||
->setDescription('Generate photo entries')
|
||||
|
@ -137,45 +107,52 @@ class Index extends Command {
|
|||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Clear existing index before creating a new one (SLOW)'
|
||||
);
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// Get options and arguments
|
||||
$refresh = $input->getOption('refresh') ? true : false;
|
||||
$clear = $input->getOption('clear') ? true : false;
|
||||
|
||||
// Clear index if asked for this
|
||||
if ($clear && $input->isInteractive()) {
|
||||
$output->write("Are you sure you want to clear the existing index? (y/N): ");
|
||||
$output->write('Are you sure you want to clear the existing index? (y/N): ');
|
||||
$answer = trim(fgets(STDIN));
|
||||
if ($answer !== 'y') {
|
||||
$output->writeln("Aborting");
|
||||
if ('y' !== $answer) {
|
||||
$output->writeln('Aborting');
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if ($clear) {
|
||||
$this->timelineWrite->clear();
|
||||
$output->writeln("Cleared existing index");
|
||||
$output->writeln('Cleared existing index');
|
||||
}
|
||||
|
||||
// Run with the static process
|
||||
try {
|
||||
\OCA\Memories\Exif::ensureStaticExiftoolProc();
|
||||
|
||||
return $this->executeWithOpts($output, $refresh);
|
||||
} catch (\Exception $e) {
|
||||
error_log("FATAL: " . $e->getMessage());
|
||||
error_log('FATAL: '.$e->getMessage());
|
||||
|
||||
return 1;
|
||||
} finally {
|
||||
\OCA\Memories\Exif::closeStaticExiftoolProc();
|
||||
}
|
||||
}
|
||||
|
||||
protected function executeWithOpts(OutputInterface $output, bool &$refresh): int {
|
||||
protected function executeWithOpts(OutputInterface $output, bool &$refresh): int
|
||||
{
|
||||
// Refuse to run without exiftool
|
||||
if (!$this->testExif()) {
|
||||
error_log('FATAL: exiftool could not be found or test failed');
|
||||
error_log('Please install exiftool (at least v12) and make sure it is in the PATH');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@ -184,6 +161,7 @@ class Index extends Command {
|
|||
|
||||
if ($this->encryptionManager->isEnabled()) {
|
||||
error_log('FATAL: Encryption is enabled. Aborted.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
$this->output = $output;
|
||||
|
@ -194,19 +172,58 @@ class Index extends Command {
|
|||
|
||||
// Show some stats
|
||||
$endTime = microtime(true);
|
||||
$execTime = intval(($endTime - $startTime)*1000)/1000 ;
|
||||
$execTime = (int) (($endTime - $startTime) * 1000) / 1000;
|
||||
$nTotal = $this->nInvalid + $this->nSkipped + $this->nProcessed;
|
||||
$this->output->writeln("==========================================");
|
||||
$this->output->writeln("Checked $nTotal files in $execTime sec");
|
||||
$this->output->writeln($this->nInvalid . " not valid media items");
|
||||
$this->output->writeln($this->nSkipped . " skipped because unmodified");
|
||||
$this->output->writeln($this->nProcessed . " (re-)processed");
|
||||
$this->output->writeln("==========================================");
|
||||
$this->output->writeln('==========================================');
|
||||
$this->output->writeln("Checked {$nTotal} files in {$execTime} sec");
|
||||
$this->output->writeln($this->nInvalid.' not valid media items');
|
||||
$this->output->writeln($this->nSkipped.' skipped because unmodified');
|
||||
$this->output->writeln($this->nProcessed.' (re-)processed');
|
||||
$this->output->writeln('==========================================');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function generateUserEntries(IUser &$user, bool &$refresh): void {
|
||||
/** Make sure exiftool is available */
|
||||
private function testExif()
|
||||
{
|
||||
$testfile = __DIR__.'/../../exiftest.jpg';
|
||||
$stream = fopen($testfile, 'r');
|
||||
if (!$stream) {
|
||||
error_log("Couldn't open Exif test file {$testfile}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$exif = null;
|
||||
|
||||
try {
|
||||
$exif = \OCA\Memories\Exif::getExifFromStream($stream);
|
||||
} catch (\Exception $e) {
|
||||
error_log("Couldn't read Exif data from test file: ".$e->getMessage());
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
if (!$exif) {
|
||||
error_log('Got blank Exif data from test file');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('2004:08:31 19:52:58' !== $exif['DateTimeOriginal']) {
|
||||
error_log('Got unexpected Exif data from test file');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function generateUserEntries(IUser &$user, bool &$refresh): void
|
||||
{
|
||||
\OC_Util::tearDownFS();
|
||||
\OC_Util::setupFS($user->getUID());
|
||||
|
||||
|
@ -215,17 +232,19 @@ class Index extends Command {
|
|||
$this->parseFolder($userFolder, $refresh);
|
||||
}
|
||||
|
||||
private function parseFolder(Folder &$folder, bool &$refresh): void {
|
||||
private function parseFolder(Folder &$folder, bool &$refresh): void
|
||||
{
|
||||
try {
|
||||
$folderPath = $folder->getPath();
|
||||
|
||||
// Respect the '.nomedia' file. If present don't traverse the folder
|
||||
if ($folder->nodeExists('.nomedia')) {
|
||||
$this->output->writeln('Skipping folder ' . $folderPath);
|
||||
$this->output->writeln('Skipping folder '.$folderPath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln('Scanning folder ' . $folderPath);
|
||||
$this->output->writeln('Scanning folder '.$folderPath);
|
||||
|
||||
$nodes = $folder->getDirectoryListing();
|
||||
|
||||
|
@ -237,21 +256,23 @@ class Index extends Command {
|
|||
}
|
||||
}
|
||||
} catch (StorageNotAvailableException $e) {
|
||||
$this->output->writeln(sprintf('<error>Storage for folder folder %s is not available: %s</error>',
|
||||
$this->output->writeln(sprintf(
|
||||
'<error>Storage for folder folder %s is not available: %s</error>',
|
||||
$folder->getPath(),
|
||||
$e->getHint()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function parseFile(File &$file, bool &$refresh): void {
|
||||
private function parseFile(File &$file, bool &$refresh): void
|
||||
{
|
||||
$res = $this->timelineWrite->processFile($file, $refresh);
|
||||
if ($res === 2) {
|
||||
$this->nProcessed++;
|
||||
} else if ($res === 1) {
|
||||
$this->nSkipped++;
|
||||
if (2 === $res) {
|
||||
++$this->nProcessed;
|
||||
} elseif (1 === $res) {
|
||||
++$this->nSkipped;
|
||||
} else {
|
||||
$this->nInvalid++;
|
||||
++$this->nInvalid;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
<?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
|
||||
|
@ -20,7 +19,6 @@ declare(strict_types=1);
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Controller;
|
||||
|
@ -32,21 +30,22 @@ use OCA\Memories\Exif;
|
|||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\DataDisplayResponse;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\StreamResponse;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\DataDisplayResponse;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IPreview;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
use OCP\IPreview;
|
||||
|
||||
class ApiController extends Controller {
|
||||
class ApiController extends Controller
|
||||
{
|
||||
private IConfig $config;
|
||||
private IUserSession $userSession;
|
||||
private IDBConnection $connection;
|
||||
|
@ -63,8 +62,9 @@ class ApiController extends Controller {
|
|||
IDBConnection $connection,
|
||||
IRootFolder $rootFolder,
|
||||
IAppManager $appManager,
|
||||
IPreview $previewManager) {
|
||||
|
||||
IPreview $previewManager
|
||||
)
|
||||
{
|
||||
parent::__construct(Application::APPNAME, $request);
|
||||
|
||||
$this->config = $config;
|
||||
|
@ -77,117 +77,23 @@ class ApiController extends Controller {
|
|||
$this->timelineWrite = new TimelineWrite($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transformations depending on the request
|
||||
*/
|
||||
private function getTransformations() {
|
||||
$transforms = array();
|
||||
|
||||
// Filter only favorites
|
||||
if ($this->request->getParam('fav')) {
|
||||
$transforms[] = array($this->timelineQuery, 'transformFavoriteFilter');
|
||||
}
|
||||
|
||||
// Filter only videos
|
||||
if ($this->request->getParam('vid')) {
|
||||
$transforms[] = array($this->timelineQuery, 'transformVideoFilter');
|
||||
}
|
||||
|
||||
// Filter only for one face
|
||||
if ($this->recognizeIsEnabled()) {
|
||||
$face = $this->request->getParam('face');
|
||||
if ($face) {
|
||||
$transforms[] = array($this->timelineQuery, 'transformFaceFilter', $face);
|
||||
}
|
||||
|
||||
$faceRect = $this->request->getParam('facerect');
|
||||
if ($faceRect) {
|
||||
$transforms[] = array($this->timelineQuery, 'transformFaceRect', $face);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter only for one tag
|
||||
if ($this->tagsIsEnabled()) {
|
||||
$tagName = $this->request->getParam('tag');
|
||||
if ($tagName) {
|
||||
$transforms[] = array($this->timelineQuery, 'transformTagFilter', $tagName);
|
||||
}
|
||||
}
|
||||
|
||||
// Limit number of responses for day query
|
||||
$limit = $this->request->getParam('limit');
|
||||
if ($limit) {
|
||||
$transforms[] = array($this->timelineQuery, 'transformLimitDay', intval($limit));
|
||||
}
|
||||
|
||||
return $transforms;
|
||||
}
|
||||
|
||||
/** Preload a few "day" at the start of "days" response */
|
||||
private function preloadDays(array &$days, Folder &$folder, bool $recursive, bool $archive) {
|
||||
$uid = $this->userSession->getUser()->getUID();
|
||||
$transforms = $this->getTransformations();
|
||||
$preloaded = 0;
|
||||
foreach ($days as &$day) {
|
||||
$day["detail"] = $this->timelineQuery->getDay(
|
||||
$folder,
|
||||
$uid,
|
||||
[$day["dayid"]],
|
||||
$recursive,
|
||||
$archive,
|
||||
$transforms,
|
||||
);
|
||||
$day["count"] = count($day["detail"]); // make sure count is accurate
|
||||
$preloaded += $day["count"];
|
||||
|
||||
if ($preloaded >= 50) { // should be enough
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function days(): JSONResponse {
|
||||
public function days(): JSONResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (null === $user) {
|
||||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
$uid = $user->getUID();
|
||||
|
||||
// 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(["message" => "Folder not found"], Http::STATUS_NOT_FOUND);
|
||||
$recursive = null === $this->request->getParam('folder');
|
||||
$archive = null !== $this->request->getParam('archive');
|
||||
if (null === $folder) {
|
||||
return new JSONResponse(['message' => 'Folder not found'], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Run actual query
|
||||
|
@ -210,56 +116,55 @@ class ApiController extends Controller {
|
|||
|
||||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function dayPost(): JSONResponse {
|
||||
public function dayPost(): JSONResponse
|
||||
{
|
||||
$id = $this->request->getParam('body_ids');
|
||||
if (is_null($id)) {
|
||||
if (null === $id) {
|
||||
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->day($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function day(string $id): JSONResponse {
|
||||
public function day(string $id): JSONResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (null === $user) {
|
||||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
$uid = $user->getUID();
|
||||
|
||||
// Check for wildcard
|
||||
$day_ids = [];
|
||||
if ($id === "*") {
|
||||
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));
|
||||
return (int) $part;
|
||||
}, explode(',', $id));
|
||||
}
|
||||
|
||||
// Check if $day_ids is empty
|
||||
if (!is_null($day_ids) && count($day_ids) === 0) {
|
||||
if (null !== $day_ids && 0 === \count($day_ids)) {
|
||||
return new JSONResponse([], Http::STATUS_OK);
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
$recursive = null === $this->request->getParam('folder');
|
||||
$archive = null !== $this->request->getParam('archive');
|
||||
if (null === $folder) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
@ -273,16 +178,18 @@ class ApiController extends Controller {
|
|||
$archive,
|
||||
$this->getTransformations(),
|
||||
);
|
||||
|
||||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subfolders entry for days response
|
||||
* Get subfolders entry for days response.
|
||||
*/
|
||||
public function getSubfoldersEntry(Folder &$folder) {
|
||||
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
|
||||
|
@ -294,20 +201,20 @@ class ApiController extends Controller {
|
|||
$folders = $view->getDirectoryContent($folder->getPath(), FileInfo::MIMETYPE_FOLDER, $folder);
|
||||
|
||||
// Sort by name
|
||||
usort($folders, function($a, $b) {
|
||||
usort($folders, function ($a, $b) {
|
||||
return strnatcmp($a->getName(), $b->getName());
|
||||
});
|
||||
|
||||
// Process to response type
|
||||
return [
|
||||
"dayid" => \OCA\Memories\Util::$TAG_DAYID_FOLDERS,
|
||||
"count" => count($folders),
|
||||
"detail" => array_map(function ($node) {
|
||||
'dayid' => \OCA\Memories\Util::$TAG_DAYID_FOLDERS,
|
||||
'count' => \count($folders),
|
||||
'detail' => array_map(function ($node) {
|
||||
return [
|
||||
"fileid" => $node->getId(),
|
||||
"name" => $node->getName(),
|
||||
"isfolder" => 1,
|
||||
"path" => $node->getPath(),
|
||||
'fileid' => $node->getId(),
|
||||
'name' => $node->getName(),
|
||||
'isfolder' => 1,
|
||||
'path' => $node->getPath(),
|
||||
];
|
||||
}, $folders, []),
|
||||
];
|
||||
|
@ -317,22 +224,22 @@ class ApiController extends Controller {
|
|||
* @NoAdminRequired
|
||||
*
|
||||
* Get list of tags with counts of images
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function tags(): JSONResponse {
|
||||
public function tags(): JSONResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (null === $user) {
|
||||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
// Check tags enabled for this user
|
||||
if (!$this->tagsIsEnabled()) {
|
||||
return new JSONResponse(["message" => "Tags not enabled for user"], Http::STATUS_PRECONDITION_FAILED);
|
||||
return new JSONResponse(['message' => 'Tags not enabled for user'], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
// If this isn't the timeline folder then things aren't going to work
|
||||
$folder = $this->getRequestFolder();
|
||||
if (is_null($folder)) {
|
||||
if (null === $folder) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
@ -347,21 +254,21 @@ class ApiController extends Controller {
|
|||
// Convert to map with key as systemtagid
|
||||
$previews_map = [];
|
||||
foreach ($previews as &$preview) {
|
||||
$key = $preview["systemtagid"];
|
||||
if (!array_key_exists($key, $previews_map)) {
|
||||
$key = $preview['systemtagid'];
|
||||
if (!\array_key_exists($key, $previews_map)) {
|
||||
$previews_map[$key] = [];
|
||||
}
|
||||
unset($preview["systemtagid"]);
|
||||
unset($preview['systemtagid']);
|
||||
$previews_map[$key][] = $preview;
|
||||
}
|
||||
|
||||
// Add previews to list
|
||||
foreach ($list as &$tag) {
|
||||
$key = $tag["id"];
|
||||
if (array_key_exists($key, $previews_map)) {
|
||||
$tag["previews"] = $previews_map[$key];
|
||||
$key = $tag['id'];
|
||||
if (\array_key_exists($key, $previews_map)) {
|
||||
$tag['previews'] = $previews_map[$key];
|
||||
} else {
|
||||
$tag["previews"] = [];
|
||||
$tag['previews'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -372,22 +279,22 @@ class ApiController extends Controller {
|
|||
* @NoAdminRequired
|
||||
*
|
||||
* Get list of faces with counts of images
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function faces(): JSONResponse {
|
||||
public function faces(): JSONResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (null === $user) {
|
||||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
// Check faces enabled for this user
|
||||
if (!$this->recognizeIsEnabled()) {
|
||||
return new JSONResponse(["message" => "Recognize app not enabled or not v3+"], Http::STATUS_PRECONDITION_FAILED);
|
||||
return new JSONResponse(['message' => 'Recognize app not enabled or not v3+'], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
// If this isn't the timeline folder then things aren't going to work
|
||||
$folder = $this->getRequestFolder();
|
||||
if (is_null($folder)) {
|
||||
if (null === $folder) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
@ -401,14 +308,17 @@ class ApiController extends Controller {
|
|||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* Get face preview image cropped with imagick
|
||||
*
|
||||
* @return DataResponse
|
||||
*/
|
||||
public function facePreview(string $id): Http\Response {
|
||||
public function facePreview(string $id): Http\Response
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (null === $user) {
|
||||
return new DataResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
|
@ -419,13 +329,13 @@ class ApiController extends Controller {
|
|||
|
||||
// Get folder to search for
|
||||
$folder = $this->getRequestFolder();
|
||||
if (is_null($folder)) {
|
||||
if (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) {
|
||||
$detections = $this->timelineQuery->getFacePreviewDetection($folder, (int) $id);
|
||||
if (null === $detections || 0 === \count($detections)) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
@ -433,8 +343,8 @@ class ApiController extends Controller {
|
|||
$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) {
|
||||
$files = $folder->getById($detection['file_id']);
|
||||
if (0 === \count($files) || FileInfo::TYPE_FILE !== $files[0]->getType()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -450,7 +360,7 @@ class ApiController extends Controller {
|
|||
}
|
||||
|
||||
// Make sure the preview is valid
|
||||
if (is_null($preview)) {
|
||||
if (null === $preview) {
|
||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
@ -459,16 +369,16 @@ class ApiController extends Controller {
|
|||
$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;
|
||||
$dw = (float) ($detection['width']);
|
||||
$dh = (float) ($detection['height']);
|
||||
$dcx = (float) ($detection['x']) + (float) ($detection['width']) / 2;
|
||||
$dcy = (float) ($detection['y']) + (float) ($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),
|
||||
(int) $faceDim,
|
||||
(int) $faceDim,
|
||||
(int) ($dcx * $iw - $faceDim / 2),
|
||||
(int) ($dcy * $ih - $faceDim / 2),
|
||||
);
|
||||
$image->scaleImage(256, 256, true);
|
||||
$blob = $image->getImageBlob();
|
||||
|
@ -478,6 +388,7 @@ class ApiController extends Controller {
|
|||
'Content-Type' => $image->getImageMimeType(),
|
||||
]);
|
||||
$response->cacheFor(3600 * 24, false, false);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
@ -485,18 +396,20 @@ class ApiController extends Controller {
|
|||
* @NoAdminRequired
|
||||
*
|
||||
* Get image info for one file
|
||||
*
|
||||
* @param string fileid
|
||||
*/
|
||||
public function imageInfo(string $id): JSONResponse {
|
||||
public function imageInfo(string $id): JSONResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (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) {
|
||||
$file = $userFolder->getById((int) $id);
|
||||
if (0 === \count($file)) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
$file = $file[0];
|
||||
|
@ -511,18 +424,20 @@ class ApiController extends Controller {
|
|||
* @NoAdminRequired
|
||||
*
|
||||
* Change exif data for one file
|
||||
*
|
||||
* @param string fileid
|
||||
*/
|
||||
public function imageEdit(string $id): JSONResponse {
|
||||
public function imageEdit(string $id): JSONResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (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) {
|
||||
$file = $userFolder->getById((int) $id);
|
||||
if (0 === \count($file)) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
$file = $file[0];
|
||||
|
@ -535,24 +450,24 @@ class ApiController extends Controller {
|
|||
// Get new date from body
|
||||
$body = $this->request->getParams();
|
||||
if (!isset($body['date'])) {
|
||||
return new JSONResponse(["message" => "Missing date"], Http::STATUS_BAD_REQUEST);
|
||||
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);
|
||||
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Update date
|
||||
try {
|
||||
$res = Exif::updateExifDate($file, $body['date']);
|
||||
if ($res === false) {
|
||||
if (false === $res) {
|
||||
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// Reprocess the file
|
||||
|
@ -565,66 +480,68 @@ class ApiController extends Controller {
|
|||
* @NoAdminRequired
|
||||
*
|
||||
* Move one file to the archive folder
|
||||
*
|
||||
* @param string fileid
|
||||
*/
|
||||
public function archive(string $id): JSONResponse {
|
||||
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);
|
||||
if (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 = $userFolder->getById((int) $id);
|
||||
if (0 === \count($file)) {
|
||||
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);
|
||||
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 (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);
|
||||
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);
|
||||
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
|
||||
$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;
|
||||
$unarchive = isset($body['archive']) && false === $body['archive'];
|
||||
|
||||
// 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) {
|
||||
$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));
|
||||
$destinationPath = substr($relativePath, \strlen($archiveFolderWithLeadingSlash));
|
||||
if (!$unarchive) {
|
||||
return new JSONResponse(["message" => "File already archived"], Http::STATUS_BAD_REQUEST);
|
||||
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);
|
||||
$destinationPath = Exif::removeExtraSlash(\OCA\Memories\Util::$ARCHIVE_FOLDER.$relativePath);
|
||||
if ($unarchive) {
|
||||
return new JSONResponse(["message" => "File not archived"], Http::STATUS_BAD_REQUEST);
|
||||
return new JSONResponse(['message' => 'File not archived'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -635,11 +552,12 @@ class ApiController extends Controller {
|
|||
// Create folder tree
|
||||
$folder = $timelineFolder;
|
||||
foreach ($destinationFolders as $folderName) {
|
||||
if ($folderName === '') {
|
||||
if ('' === $folderName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$existingFolder = $folder->get($folderName . '/');
|
||||
$existingFolder = $folder->get($folderName.'/');
|
||||
if (!$existingFolder instanceof Folder) {
|
||||
throw new \OCP\Files\NotFoundException('Not a folder');
|
||||
}
|
||||
|
@ -648,46 +566,27 @@ class ApiController extends Controller {
|
|||
try {
|
||||
$folder = $folder->newFolder($folderName);
|
||||
} catch (\OCP\Files\NotPermittedException $e) {
|
||||
return new JSONResponse(["message" => "Failed to create folder"], Http::STATUS_FORBIDDEN);
|
||||
return new JSONResponse(['message' => 'Failed to create folder'], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move file to archive folder
|
||||
try {
|
||||
$file->move($folder->getPath() . '/' . $file->getName());
|
||||
$file->move($folder->getPath().'/'.$file->getName());
|
||||
} catch (\OCP\Files\NotPermittedException $e) {
|
||||
return new JSONResponse(["message" => "Failed to move file"], Http::STATUS_FORBIDDEN);
|
||||
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);
|
||||
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);
|
||||
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(['message' => 'File is locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return new JSONResponse([], Http::STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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", ">=");
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
|
@ -698,32 +597,157 @@ class ApiController extends Controller {
|
|||
*
|
||||
* @return JSONResponse an empty JSONResponse with respective http status code
|
||||
*/
|
||||
public function setUserConfig(string $key, string $value): JSONResponse {
|
||||
public function setUserConfig(string $key, string $value): JSONResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function serviceWorker(): StreamResponse {
|
||||
public function serviceWorker(): StreamResponse
|
||||
{
|
||||
$response = new StreamResponse(__DIR__.'/../../js/memories-service-worker.js');
|
||||
$response->setHeaders([
|
||||
'Content-Type' => 'application/javascript',
|
||||
'Service-Worker-Allowed' => '/'
|
||||
'Service-Worker-Allowed' => '/',
|
||||
]);
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedWorkerSrcDomain("'self'");
|
||||
$policy->addAllowedScriptDomain("'self'");
|
||||
$policy->addAllowedConnectDomain("'self'");
|
||||
$response->setContentSecurityPolicy($policy);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transformations depending on the request.
|
||||
*/
|
||||
private function getTransformations()
|
||||
{
|
||||
$transforms = [];
|
||||
|
||||
// Filter only favorites
|
||||
if ($this->request->getParam('fav')) {
|
||||
$transforms[] = [$this->timelineQuery, 'transformFavoriteFilter'];
|
||||
}
|
||||
|
||||
// Filter only videos
|
||||
if ($this->request->getParam('vid')) {
|
||||
$transforms[] = [$this->timelineQuery, 'transformVideoFilter'];
|
||||
}
|
||||
|
||||
// Filter only for one face
|
||||
if ($this->recognizeIsEnabled()) {
|
||||
$face = $this->request->getParam('face');
|
||||
if ($face) {
|
||||
$transforms[] = [$this->timelineQuery, 'transformFaceFilter', $face];
|
||||
}
|
||||
|
||||
$faceRect = $this->request->getParam('facerect');
|
||||
if ($faceRect) {
|
||||
$transforms[] = [$this->timelineQuery, 'transformFaceRect', $face];
|
||||
}
|
||||
}
|
||||
|
||||
// Filter only for one tag
|
||||
if ($this->tagsIsEnabled()) {
|
||||
$tagName = $this->request->getParam('tag');
|
||||
if ($tagName) {
|
||||
$transforms[] = [$this->timelineQuery, 'transformTagFilter', $tagName];
|
||||
}
|
||||
}
|
||||
|
||||
// Limit number of responses for day query
|
||||
$limit = $this->request->getParam('limit');
|
||||
if ($limit) {
|
||||
$transforms[] = [$this->timelineQuery, 'transformLimitDay', (int) $limit];
|
||||
}
|
||||
|
||||
return $transforms;
|
||||
}
|
||||
|
||||
/** Preload a few "day" at the start of "days" response */
|
||||
private function preloadDays(array &$days, Folder &$folder, bool $recursive, bool $archive)
|
||||
{
|
||||
$uid = $this->userSession->getUser()->getUID();
|
||||
$transforms = $this->getTransformations();
|
||||
$preloaded = 0;
|
||||
foreach ($days as &$day) {
|
||||
$day['detail'] = $this->timelineQuery->getDay(
|
||||
$folder,
|
||||
$uid,
|
||||
[$day['dayid']],
|
||||
$recursive,
|
||||
$archive,
|
||||
$transforms,
|
||||
);
|
||||
$day['count'] = \count($day['detail']); // make sure count is accurate
|
||||
$preloaded += $day['count'];
|
||||
|
||||
if ($preloaded >= 50) { // should be enough
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 (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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', '>=');
|
||||
}
|
||||
}
|
|
@ -1,26 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Memories\Controller;
|
||||
|
||||
use OCP\IRequest;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCA\Viewer\Event\LoadViewer;
|
||||
use OCA\Files\Event\LoadSidebar;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCA\Memories\AppInfo\Application;
|
||||
use OCA\Viewer\Event\LoadViewer;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Util;
|
||||
|
||||
use OCA\Memories\AppInfo\Application;
|
||||
|
||||
class PageController extends Controller {
|
||||
class PageController extends Controller
|
||||
{
|
||||
protected $userId;
|
||||
protected $appName;
|
||||
private IAppManager $appManager;
|
||||
protected IEventDispatcher $eventDispatcher;
|
||||
private IAppManager $appManager;
|
||||
private IInitialState $initialState;
|
||||
private IUserSession $userSession;
|
||||
private IConfig $config;
|
||||
|
@ -33,8 +34,9 @@ class PageController extends Controller {
|
|||
IEventDispatcher $eventDispatcher,
|
||||
IInitialState $initialState,
|
||||
IUserSession $userSession,
|
||||
IConfig $config) {
|
||||
|
||||
IConfig $config
|
||||
)
|
||||
{
|
||||
parent::__construct($AppName, $request);
|
||||
$this->userId = $UserId;
|
||||
$this->appName = $AppName;
|
||||
|
@ -47,11 +49,13 @@ class PageController extends Controller {
|
|||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function main() {
|
||||
public function main()
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if (is_null($user)) {
|
||||
if (null === $user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -65,12 +69,16 @@ class PageController extends Controller {
|
|||
$timelinePath = \OCA\Memories\Util::getPhotosPath($this->config, $uid);
|
||||
$this->initialState->provideInitialState('timelinePath', $timelinePath);
|
||||
$this->initialState->provideInitialState('showHidden', $this->config->getUserValue(
|
||||
$uid, Application::APPNAME, 'showHidden', false));
|
||||
$uid,
|
||||
Application::APPNAME,
|
||||
'showHidden',
|
||||
false
|
||||
));
|
||||
|
||||
// Apps enabled
|
||||
$this->initialState->provideInitialState('systemtags', $this->appManager->isEnabledForUser('systemtags') === true);
|
||||
$this->initialState->provideInitialState('recognize', $this->appManager->isEnabledForUser('recognize') === true);
|
||||
$this->initialState->provideInitialState('version', $this->appManager->getAppInfo('memories')["version"]);
|
||||
$this->initialState->provideInitialState('systemtags', true === $this->appManager->isEnabledForUser('systemtags'));
|
||||
$this->initialState->provideInitialState('recognize', true === $this->appManager->isEnabledForUser('recognize'));
|
||||
$this->initialState->provideInitialState('version', $this->appManager->getAppInfo('memories')['version']);
|
||||
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedWorkerSrcDomain("'self'");
|
||||
|
@ -78,62 +86,77 @@ class PageController extends Controller {
|
|||
|
||||
$response = new TemplateResponse($this->appName, 'main');
|
||||
$response->setContentSecurityPolicy($policy);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function folder() {
|
||||
public function folder()
|
||||
{
|
||||
return $this->main();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function favorites() {
|
||||
public function favorites()
|
||||
{
|
||||
return $this->main();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function videos() {
|
||||
public function videos()
|
||||
{
|
||||
return $this->main();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function archive() {
|
||||
public function archive()
|
||||
{
|
||||
return $this->main();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function thisday() {
|
||||
public function thisday()
|
||||
{
|
||||
return $this->main();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function people() {
|
||||
public function people()
|
||||
{
|
||||
return $this->main();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function tags() {
|
||||
public function tags()
|
||||
{
|
||||
return $this->main();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class TimelineQuery {
|
||||
class TimelineQuery
|
||||
{
|
||||
use TimelineQueryDays;
|
||||
use TimelineQueryFaces;
|
||||
use TimelineQueryFilters;
|
||||
use TimelineQueryTags;
|
||||
use TimelineQueryFaces;
|
||||
|
||||
protected IDBConnection $connection;
|
||||
|
||||
public function __construct(IDBConnection $connection) {
|
||||
public function __construct(IDBConnection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function getInfoById(int $id): array {
|
||||
public function getInfoById(int $id): array
|
||||
{
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('fileid', 'dayid', 'datetaken')
|
||||
->from('memories')
|
||||
->where($qb->expr()->eq('fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT)));
|
||||
->where($qb->expr()->eq('fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT)))
|
||||
;
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
$row = $result->fetch();
|
||||
$result->closeCursor();
|
||||
|
||||
$utcTs = 0;
|
||||
|
||||
try {
|
||||
$utcDate = new \DateTime($row['datetaken'], new \DateTimeZone('UTC'));
|
||||
$utcTs = $utcDate->getTimestamp();
|
||||
} catch (\Throwable $e) {}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return [
|
||||
'fileid' => intval($row['fileid']),
|
||||
'dayid' => intval($row['dayid']),
|
||||
'fileid' => (int) ($row['fileid']),
|
||||
'dayid' => (int) ($row['dayid']),
|
||||
'datetaken' => $utcTs,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,117 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\IDBConnection;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
trait TimelineQueryDays {
|
||||
trait TimelineQueryDays
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
/**
|
||||
* Process the days response
|
||||
* @param array $days
|
||||
*/
|
||||
private function processDays(&$days) {
|
||||
foreach($days as &$row) {
|
||||
$row["dayid"] = intval($row["dayid"]);
|
||||
$row["count"] = intval($row["count"]);
|
||||
|
||||
// All transform processing
|
||||
$this->processFace($row, true);
|
||||
}
|
||||
return $days;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the single day response
|
||||
* @param array $day
|
||||
*/
|
||||
private function processDay(&$day) {
|
||||
foreach($day as &$row) {
|
||||
// We don't need date taken (see query builder)
|
||||
unset($row['datetaken']);
|
||||
|
||||
// Convert field types
|
||||
$row["fileid"] = intval($row["fileid"]);
|
||||
$row["isvideo"] = intval($row["isvideo"]);
|
||||
$row["dayid"] = intval($row["dayid"]);
|
||||
$row["w"] = intval($row["w"]);
|
||||
$row["h"] = intval($row["h"]);
|
||||
if (!$row["isvideo"]) {
|
||||
unset($row["isvideo"]);
|
||||
}
|
||||
if ($row["categoryid"]) {
|
||||
$row["isfavorite"] = 1;
|
||||
}
|
||||
unset($row["categoryid"]);
|
||||
|
||||
// All transform processing
|
||||
$this->processFace($row);
|
||||
}
|
||||
return $day;
|
||||
}
|
||||
|
||||
/** Get the query for oc_filecache join */
|
||||
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(
|
||||
$subQuery->expr()->eq('fileid', $subQuery->createNamedParameter($folder->getId())),
|
||||
)->executeQuery();
|
||||
$finfo = $cursor->fetch();
|
||||
$cursor->closeCursor();
|
||||
if (empty($finfo)) {
|
||||
throw new \Exception("Folder not found");
|
||||
}
|
||||
|
||||
$pathQuery = null;
|
||||
if ($recursive) {
|
||||
// Filter by path for recursive query
|
||||
$likePath = $finfo["path"];
|
||||
if (!empty($likePath)) {
|
||||
$likePath .= '/';
|
||||
}
|
||||
$pathQuery = $query->expr()->like('f.path', $query->createNamedParameter($likePath . '%'));
|
||||
|
||||
// Exclude/show archive folder
|
||||
$archiveLikePath = $likePath . \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));
|
||||
}
|
||||
|
||||
return $query->expr()->andX(
|
||||
$query->expr()->eq('f.fileid', 'm.fileid'),
|
||||
$query->expr()->in('f.storage', $query->createNamedParameter($finfo["storage"])),
|
||||
$pathQuery,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the days response from the database for the timeline
|
||||
* Get the days response from the database for the timeline.
|
||||
*
|
||||
* @param 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
|
||||
*
|
||||
* @return array The days response
|
||||
*/
|
||||
public function getDays(
|
||||
|
@ -127,11 +35,13 @@ 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, $archive));
|
||||
->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive, $archive))
|
||||
;
|
||||
|
||||
// Group and sort by dayid
|
||||
$query->groupBy('m.dayid')
|
||||
->orderBy('m.dayid', 'DESC');
|
||||
->orderBy('m.dayid', 'DESC')
|
||||
;
|
||||
|
||||
// Apply all transformations
|
||||
$this->applyAllTransforms($queryTransforms, $query, $uid);
|
||||
|
@ -139,17 +49,21 @@ trait TimelineQueryDays {
|
|||
$cursor = $query->executeQuery();
|
||||
$rows = $cursor->fetchAll();
|
||||
$cursor->closeCursor();
|
||||
|
||||
return $this->processDays($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the day response from the database for the timeline
|
||||
* Get the day response from the database for the timeline.
|
||||
*
|
||||
* @param Folder $folder The folder to get the day from
|
||||
* @param string $uid The user id
|
||||
* @param int[] $dayid The day id
|
||||
* @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(
|
||||
|
@ -170,10 +84,11 @@ trait TimelineQueryDays {
|
|||
// when using DISTINCT on selected fields
|
||||
$query->select($fileid, 'f.etag', 'm.isvideo', 'vco.categoryid', 'm.datetaken', 'm.dayid', 'm.w', 'm.h')
|
||||
->from('memories', 'm')
|
||||
->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive, $archive));
|
||||
->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive, $archive))
|
||||
;
|
||||
|
||||
// Filter by dayid unless wildcard
|
||||
if (!is_null($day_ids)) {
|
||||
if (null !== $day_ids) {
|
||||
$query->andWhere($query->expr()->in('m.dayid', $query->createNamedParameter($day_ids, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
} else {
|
||||
// Limit wildcard to 100 results
|
||||
|
@ -193,6 +108,108 @@ trait TimelineQueryDays {
|
|||
$cursor = $query->executeQuery();
|
||||
$rows = $cursor->fetchAll();
|
||||
$cursor->closeCursor();
|
||||
|
||||
return $this->processDay($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the days response.
|
||||
*
|
||||
* @param array $days
|
||||
*/
|
||||
private function processDays(&$days)
|
||||
{
|
||||
foreach ($days as &$row) {
|
||||
$row['dayid'] = (int) ($row['dayid']);
|
||||
$row['count'] = (int) ($row['count']);
|
||||
|
||||
// All transform processing
|
||||
$this->processFace($row, true);
|
||||
}
|
||||
|
||||
return $days;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the single day response.
|
||||
*
|
||||
* @param array $day
|
||||
*/
|
||||
private function processDay(&$day)
|
||||
{
|
||||
foreach ($day as &$row) {
|
||||
// We don't need date taken (see query builder)
|
||||
unset($row['datetaken']);
|
||||
|
||||
// Convert field types
|
||||
$row['fileid'] = (int) ($row['fileid']);
|
||||
$row['isvideo'] = (int) ($row['isvideo']);
|
||||
$row['dayid'] = (int) ($row['dayid']);
|
||||
$row['w'] = (int) ($row['w']);
|
||||
$row['h'] = (int) ($row['h']);
|
||||
if (!$row['isvideo']) {
|
||||
unset($row['isvideo']);
|
||||
}
|
||||
if ($row['categoryid']) {
|
||||
$row['isfavorite'] = 1;
|
||||
}
|
||||
unset($row['categoryid']);
|
||||
|
||||
// All transform processing
|
||||
$this->processFace($row);
|
||||
}
|
||||
|
||||
return $day;
|
||||
}
|
||||
|
||||
/** Get the query for oc_filecache join */
|
||||
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(
|
||||
$subQuery->expr()->eq('fileid', $subQuery->createNamedParameter($folder->getId())),
|
||||
)->executeQuery();
|
||||
$finfo = $cursor->fetch();
|
||||
$cursor->closeCursor();
|
||||
if (empty($finfo)) {
|
||||
throw new \Exception('Folder not found');
|
||||
}
|
||||
|
||||
$pathQuery = null;
|
||||
if ($recursive) {
|
||||
// Filter by path for recursive query
|
||||
$likePath = $finfo['path'];
|
||||
if (!empty($likePath)) {
|
||||
$likePath .= '/';
|
||||
}
|
||||
$pathQuery = $query->expr()->like('f.path', $query->createNamedParameter($likePath.'%'));
|
||||
|
||||
// Exclude/show archive folder
|
||||
$archiveLikePath = $likePath.\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));
|
||||
}
|
||||
|
||||
return $query->expr()->andX(
|
||||
$query->expr()->eq('f.fileid', 'm.fileid'),
|
||||
$query->expr()->in('f.storage', $query->createNamedParameter($finfo['storage'])),
|
||||
$pathQuery,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\IDBConnection;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
trait TimelineQueryFaces {
|
||||
trait TimelineQueryFaces
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
public function transformFaceFilter(IQueryBuilder &$query, string $userId, string $faceStr) {
|
||||
public function transformFaceFilter(IQueryBuilder &$query, string $userId, string $faceStr)
|
||||
{
|
||||
// Get title and uid of face user
|
||||
$faceNames = explode('/', $faceStr);
|
||||
if (count($faceNames) !== 2) throw new \Exception("Invalid face query");
|
||||
if (2 !== \count($faceNames)) {
|
||||
throw new \Exception('Invalid face query');
|
||||
}
|
||||
$faceUid = $faceNames[0];
|
||||
$faceName = $faceNames[1];
|
||||
|
||||
|
@ -31,7 +36,8 @@ trait TimelineQueryFaces {
|
|||
));
|
||||
}
|
||||
|
||||
public function transformFaceRect(IQueryBuilder &$query, string $userId) {
|
||||
public function transformFaceRect(IQueryBuilder &$query, string $userId)
|
||||
{
|
||||
// Include detection params in response
|
||||
$query->addSelect(
|
||||
'rfd.width AS face_w',
|
||||
|
@ -41,26 +47,8 @@ trait TimelineQueryFaces {
|
|||
);
|
||||
}
|
||||
|
||||
/** Convert face fields to object */
|
||||
private function processFace(&$row, $days=false) {
|
||||
if (!isset($row) || !isset($row['face_w'])) return;
|
||||
|
||||
if (!$days) {
|
||||
$row["facerect"] = [
|
||||
"w" => floatval($row["face_w"]),
|
||||
"h" => floatval($row["face_h"]),
|
||||
"x" => floatval($row["face_x"]),
|
||||
"y" => floatval($row["face_y"]),
|
||||
];
|
||||
}
|
||||
|
||||
unset($row["face_w"]);
|
||||
unset($row["face_h"]);
|
||||
unset($row["face_x"]);
|
||||
unset($row["face_y"]);
|
||||
}
|
||||
|
||||
public function getFaces(Folder $folder) {
|
||||
public function getFaces(Folder $folder)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
// SELECT all face clusters
|
||||
|
@ -88,25 +76,31 @@ trait TimelineQueryFaces {
|
|||
$faces = $query->executeQuery()->fetchAll();
|
||||
|
||||
// Post process
|
||||
foreach($faces as &$row) {
|
||||
$row['id'] = intval($row['id']);
|
||||
$row["name"] = $row["title"];
|
||||
unset($row["title"]);
|
||||
$row["count"] = intval($row["count"]);
|
||||
foreach ($faces as &$row) {
|
||||
$row['id'] = (int) ($row['id']);
|
||||
$row['name'] = $row['title'];
|
||||
unset($row['title']);
|
||||
$row['count'] = (int) ($row['count']);
|
||||
}
|
||||
|
||||
return $faces;
|
||||
}
|
||||
|
||||
public function getFacePreviewDetection(Folder &$folder, int $id) {
|
||||
public function getFacePreviewDetection(Folder &$folder, int $id)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
// SELECT face detections for ID
|
||||
$query->select(
|
||||
'rfd.file_id', // Needed to get the actual file
|
||||
'rfd.x', 'rfd.y', 'rfd.width', 'rfd.height', // Image cropping
|
||||
'm.w as image_width', 'm.h as image_height', // Scoring
|
||||
'm.fileid', 'm.datetaken', // Just in case, for postgres
|
||||
'rfd.file_id', // Get actual file
|
||||
'rfd.x', // Image cropping
|
||||
'rfd.y',
|
||||
'rfd.width',
|
||||
'rfd.height',
|
||||
'm.w as image_width', // Scoring
|
||||
'm.h as image_height',
|
||||
'm.fileid',
|
||||
'm.datetaken', // Just in case, for postgres
|
||||
)->from('recognize_face_detections', 'rfd');
|
||||
$query->where($query->expr()->eq('rfd.cluster_id', $query->createNamedParameter($id)));
|
||||
|
||||
|
@ -132,31 +126,50 @@ trait TimelineQueryFaces {
|
|||
// Score the face detections
|
||||
foreach ($previews as &$p) {
|
||||
// Get actual pixel size of face
|
||||
$iw = min(intval($p["image_width"] ?: 512), 2048);
|
||||
$ih = min(intval($p["image_height"] ?: 512), 2048);
|
||||
$w = floatval($p["width"]) * $iw;
|
||||
$h = floatval($p["height"]) * $ih;
|
||||
$iw = min((int) ($p['image_width'] ?: 512), 2048);
|
||||
$ih = min((int) ($p['image_height'] ?: 512), 2048);
|
||||
$w = (float) ($p['width']) * $iw;
|
||||
$h = (float) ($p['height']) * $ih;
|
||||
|
||||
// Get center of face
|
||||
$x = floatval($p["x"]) + floatval($p["width"]) / 2;
|
||||
$y = floatval($p["y"]) + floatval($p["height"]) / 2;
|
||||
$x = (float) ($p['x']) + (float) ($p['width']) / 2;
|
||||
$y = (float) ($p['y']) + (float) ($p['height']) / 2;
|
||||
|
||||
// 3D normal distribution - if the face is closer to the center, it's better
|
||||
$positionScore = exp(-pow($x - 0.5, 2) * 4) * exp(-pow($y - 0.5, 2) * 4);
|
||||
$positionScore = exp(-($x - 0.5) ** 2 * 4) * exp(-($y - 0.5) ** 2 * 4);
|
||||
|
||||
// Root size distribution - if the face is bigger, it's better,
|
||||
// but it doesn't matter beyond a certain point, especially 256px ;)
|
||||
$sizeScore = pow($w * 100, 1/4) * pow($h * 100, 1/4);
|
||||
$sizeScore = ($w * 100) ** (1 / 4) * ($h * 100) ** (1 / 4);
|
||||
|
||||
// Combine scores
|
||||
$p["score"] = $positionScore * $sizeScore;
|
||||
$p['score'] = $positionScore * $sizeScore;
|
||||
}
|
||||
|
||||
// Sort previews by score descending
|
||||
usort($previews, function($a, $b) {
|
||||
return $b["score"] <=> $a["score"];
|
||||
usort($previews, function ($a, $b) {
|
||||
return $b['score'] <=> $a['score'];
|
||||
});
|
||||
|
||||
return $previews;
|
||||
}
|
||||
|
||||
/** Convert face fields to object */
|
||||
private function processFace(&$row, $days = false)
|
||||
{
|
||||
if (!isset($row) || !isset($row['face_w'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$days) {
|
||||
$row['facerect'] = [
|
||||
'w' => (float) ($row['face_w']),
|
||||
'h' => (float) ($row['face_h']),
|
||||
'x' => (float) ($row['face_x']),
|
||||
'y' => (float) ($row['face_y']),
|
||||
];
|
||||
}
|
||||
|
||||
unset($row['face_w'], $row['face_h'], $row['face_x'], $row['face_y']);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
@ -6,50 +7,59 @@ namespace OCA\Memories\Db;
|
|||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\ITags;
|
||||
|
||||
trait TimelineQueryFilters {
|
||||
private function applyAllTransforms(array $transforms, IQueryBuilder &$query, string $uid): void {
|
||||
foreach ($transforms as &$transform) {
|
||||
$fun = array_slice($transform, 0, 2);
|
||||
$params = array_slice($transform, 2);
|
||||
array_unshift($params, $uid);
|
||||
array_unshift($params, $query);
|
||||
$fun(...$params);
|
||||
}
|
||||
}
|
||||
|
||||
public function transformFavoriteFilter(IQueryBuilder &$query, string $userId) {
|
||||
trait TimelineQueryFilters
|
||||
{
|
||||
public function transformFavoriteFilter(IQueryBuilder &$query, string $userId)
|
||||
{
|
||||
$query->innerJoin('m', 'vcategory_to_object', 'vcoi', $query->expr()->andX(
|
||||
$query->expr()->eq('vcoi.objid', 'm.fileid'),
|
||||
$query->expr()->in('vcoi.categoryid', $this->getFavoriteVCategoryFun($query, $userId)),
|
||||
));
|
||||
}
|
||||
|
||||
public function addFavoriteTag(IQueryBuilder &$query, string $userId) {
|
||||
public function addFavoriteTag(IQueryBuilder &$query, string $userId)
|
||||
{
|
||||
$query->leftJoin('m', 'vcategory_to_object', 'vco', $query->expr()->andX(
|
||||
$query->expr()->eq('vco.objid', 'm.fileid'),
|
||||
$query->expr()->in('vco.categoryid', $this->getFavoriteVCategoryFun($query, $userId)),
|
||||
));
|
||||
}
|
||||
|
||||
private function getFavoriteVCategoryFun(IQueryBuilder &$query, string $userId) {
|
||||
return $query->createFunction(
|
||||
$query->getConnection()->getQueryBuilder()->select('id')->from('vcategory', 'vc')->where(
|
||||
$query->expr()->andX(
|
||||
$query->expr()->eq('type', $query->createNamedParameter("files")),
|
||||
$query->expr()->eq('uid', $query->createNamedParameter($userId)),
|
||||
$query->expr()->eq('category', $query->createNamedParameter(ITags::TAG_FAVORITE)),
|
||||
))->getSQL());
|
||||
}
|
||||
|
||||
public function transformVideoFilter(IQueryBuilder &$query, string $userId) {
|
||||
public function transformVideoFilter(IQueryBuilder &$query, string $userId)
|
||||
{
|
||||
$query->andWhere($query->expr()->eq('m.isvideo', $query->createNamedParameter('1')));
|
||||
}
|
||||
|
||||
public function transformLimitDay(IQueryBuilder &$query, string $userId, int $limit) {
|
||||
public function transformLimitDay(IQueryBuilder &$query, string $userId, int $limit)
|
||||
{
|
||||
// The valid range for limit is 1 - 100; otherwise abort
|
||||
if ($limit < 1 || $limit > 100) {
|
||||
return;
|
||||
}
|
||||
$query->setMaxResults($limit);
|
||||
}
|
||||
|
||||
private function applyAllTransforms(array $transforms, IQueryBuilder &$query, string $uid): void
|
||||
{
|
||||
foreach ($transforms as &$transform) {
|
||||
$fun = \array_slice($transform, 0, 2);
|
||||
$params = \array_slice($transform, 2);
|
||||
array_unshift($params, $uid);
|
||||
array_unshift($params, $query);
|
||||
$fun(...$params);
|
||||
}
|
||||
}
|
||||
|
||||
private function getFavoriteVCategoryFun(IQueryBuilder &$query, string $userId)
|
||||
{
|
||||
return $query->createFunction(
|
||||
$query->getConnection()->getQueryBuilder()->select('id')->from('vcategory', 'vc')->where(
|
||||
$query->expr()->andX(
|
||||
$query->expr()->eq('type', $query->createNamedParameter('files')),
|
||||
$query->expr()->eq('uid', $query->createNamedParameter($userId)),
|
||||
$query->expr()->eq('category', $query->createNamedParameter(ITags::TAG_FAVORITE)),
|
||||
)
|
||||
)->getSQL()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,38 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\IDBConnection;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
trait TimelineQueryTags {
|
||||
trait TimelineQueryTags
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
public function getSystemTagId(IQueryBuilder &$query, string $tagName) {
|
||||
public function getSystemTagId(IQueryBuilder &$query, string $tagName)
|
||||
{
|
||||
$sqb = $query->getConnection()->getQueryBuilder();
|
||||
|
||||
return $sqb->select('id')->from('systemtag')->where(
|
||||
$sqb->expr()->andX(
|
||||
$sqb->expr()->eq('name', $sqb->createNamedParameter($tagName)),
|
||||
$sqb->expr()->eq('visibility', $sqb->createNamedParameter(1)),
|
||||
))->executeQuery()->fetchOne();
|
||||
)
|
||||
)->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
public function transformTagFilter(IQueryBuilder &$query, string $userId, string $tagName) {
|
||||
public function transformTagFilter(IQueryBuilder &$query, string $userId, string $tagName)
|
||||
{
|
||||
$tagId = $this->getSystemTagId($query, $tagName);
|
||||
if ($tagId === FALSE) {
|
||||
throw new \Exception("Tag $tagName not found");
|
||||
if (false === $tagId) {
|
||||
throw new \Exception("Tag {$tagName} not found");
|
||||
}
|
||||
|
||||
$query->innerJoin('m', 'systemtag_object_mapping', 'stom', $query->expr()->andX(
|
||||
$query->expr()->eq('stom.objecttype', $query->createNamedParameter("files")),
|
||||
$query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')),
|
||||
$query->expr()->eq('stom.objectid', 'm.fileid'),
|
||||
$query->expr()->eq('stom.systemtagid', $query->createNamedParameter($tagId)),
|
||||
));
|
||||
}
|
||||
|
||||
public function getTags(Folder $folder) {
|
||||
public function getTags(Folder $folder)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
// SELECT visible tag name and count of photos
|
||||
|
@ -44,7 +51,7 @@ trait TimelineQueryTags {
|
|||
// WHERE there are items with this tag
|
||||
$query->innerJoin('st', 'systemtag_object_mapping', 'stom', $query->expr()->andX(
|
||||
$query->expr()->eq('stom.systemtagid', 'st.id'),
|
||||
$query->expr()->eq('stom.objecttype', $query->createNamedParameter("files")),
|
||||
$query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')),
|
||||
));
|
||||
|
||||
// WHERE these items are memories indexed photos
|
||||
|
@ -62,15 +69,16 @@ trait TimelineQueryTags {
|
|||
$tags = $query->executeQuery()->fetchAll();
|
||||
|
||||
// Post process
|
||||
foreach($tags as &$row) {
|
||||
$row["id"] = intval($row["id"]);
|
||||
$row["count"] = intval($row["count"]);
|
||||
foreach ($tags as &$row) {
|
||||
$row['id'] = (int) ($row['id']);
|
||||
$row['count'] = (int) ($row['count']);
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
public function getTagPreviews(Folder $folder) {
|
||||
public function getTagPreviews(Folder $folder)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
// Windowing
|
||||
|
@ -78,8 +86,10 @@ trait TimelineQueryTags {
|
|||
|
||||
// SELECT all photos with this tag
|
||||
$query->select('f.fileid', 'f.etag', 'stom.systemtagid', $rowNumber)->from(
|
||||
'systemtag_object_mapping', 'stom')->where(
|
||||
$query->expr()->eq('stom.objecttype', $query->createNamedParameter("files")),
|
||||
'systemtag_object_mapping',
|
||||
'stom'
|
||||
)->where(
|
||||
$query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')),
|
||||
);
|
||||
|
||||
// WHERE these items are memories indexed photos
|
||||
|
@ -89,7 +99,7 @@ trait TimelineQueryTags {
|
|||
$query->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, true, false));
|
||||
|
||||
// Make this a sub query
|
||||
$fun = $query->createFunction('(' . $query->getSQL() . ')');
|
||||
$fun = $query->createFunction('('.$query->getSQL().')');
|
||||
|
||||
// Create outer query
|
||||
$outerQuery = $this->connection->getQueryBuilder();
|
||||
|
@ -102,10 +112,10 @@ trait TimelineQueryTags {
|
|||
$previews = $outerQuery->executeQuery()->fetchAll();
|
||||
|
||||
// Post-process
|
||||
foreach($previews as &$row) {
|
||||
$row["fileid"] = intval($row["fileid"]);
|
||||
$row["systemtagid"] = intval($row["systemtagid"]);
|
||||
unset($row["n"]);
|
||||
foreach ($previews as &$row) {
|
||||
$row['fileid'] = (int) ($row['fileid']);
|
||||
$row['systemtagid'] = (int) ($row['systemtagid']);
|
||||
unset($row['n']);
|
||||
}
|
||||
|
||||
return $previews;
|
||||
|
|
|
@ -1,44 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCA\Memories\AppInfo\Application;
|
||||
use OCA\Memories\Exif;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Files\File;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
|
||||
class TimelineWrite {
|
||||
class TimelineWrite
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
public function __construct(IDBConnection $connection) {
|
||||
public function __construct(IDBConnection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file has a valid mimetype for processing
|
||||
* @param File $file
|
||||
* Check if a file has a valid mimetype for processing.
|
||||
*
|
||||
* @return int 0 for invalid, 1 for image, 2 for video
|
||||
*/
|
||||
public function getFileType(File $file): int {
|
||||
public function getFileType(File $file): int
|
||||
{
|
||||
$mime = $file->getMimeType();
|
||||
if (in_array($mime, Application::IMAGE_MIMES)) {
|
||||
if (\in_array($mime, Application::IMAGE_MIMES, true)) {
|
||||
return 1;
|
||||
} elseif (in_array($mime, Application::VIDEO_MIMES)) {
|
||||
}
|
||||
if (\in_array($mime, Application::VIDEO_MIMES, true)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a file to insert Exif data into the database
|
||||
* @param File $file
|
||||
* Process a file to insert Exif data into the database.
|
||||
*
|
||||
* @return int 2 if processed, 1 if skipped, 0 if not valid
|
||||
*/
|
||||
public function processFile(
|
||||
File &$file,
|
||||
bool $force=false
|
||||
bool $force = false
|
||||
): int {
|
||||
// There is no easy way to UPSERT in a standard SQL way, so just
|
||||
// do multiple calls. The worst that can happen is more updates,
|
||||
|
@ -47,7 +53,7 @@ class TimelineWrite {
|
|||
|
||||
// Check if we want to process this file
|
||||
$fileType = $this->getFileType($file);
|
||||
$isvideo = ($fileType === 2);
|
||||
$isvideo = (2 === $fileType);
|
||||
if (!$fileType) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -60,19 +66,22 @@ class TimelineWrite {
|
|||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('fileid', 'mtime')
|
||||
->from('memories')
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$cursor = $query->executeQuery();
|
||||
$prevRow = $cursor->fetch();
|
||||
$cursor->closeCursor();
|
||||
if ($prevRow && !$force && intval($prevRow['mtime']) === $mtime) {
|
||||
if ($prevRow && !$force && (int) ($prevRow['mtime']) === $mtime) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get exif data
|
||||
$exif = [];
|
||||
|
||||
try {
|
||||
$exif = Exif::getExifFromFile($file);
|
||||
} catch (\Exception $e) {}
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
// Get more parameters
|
||||
$dateTaken = Exif::getDateTaken($file, $exif);
|
||||
|
@ -89,7 +98,8 @@ class TimelineWrite {
|
|||
->set('isvideo', $query->createNamedParameter($isvideo, IQueryBuilder::PARAM_INT))
|
||||
->set('w', $query->createNamedParameter($w, IQueryBuilder::PARAM_INT))
|
||||
->set('h', $query->createNamedParameter($h, IQueryBuilder::PARAM_INT))
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$query->executeStatement();
|
||||
} else {
|
||||
// Try to create new row
|
||||
|
@ -103,10 +113,11 @@ class TimelineWrite {
|
|||
'isvideo' => $query->createNamedParameter($isvideo, IQueryBuilder::PARAM_INT),
|
||||
'w' => $query->createNamedParameter($w, IQueryBuilder::PARAM_INT),
|
||||
'h' => $query->createNamedParameter($h, IQueryBuilder::PARAM_INT),
|
||||
]);
|
||||
])
|
||||
;
|
||||
$query->executeStatement();
|
||||
} catch (\Exception $ex) {
|
||||
error_log("Failed to create memories record: " . $ex->getMessage());
|
||||
error_log('Failed to create memories record: '.$ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,21 +125,24 @@ class TimelineWrite {
|
|||
}
|
||||
|
||||
/**
|
||||
* Remove a file from the exif database
|
||||
* @param File $file
|
||||
* Remove a file from the exif database.
|
||||
*/
|
||||
public function deleteFile(File &$file) {
|
||||
public function deleteFile(File &$file)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->delete('memories')
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT)));
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire index. Does not need confirmation!
|
||||
*
|
||||
* @param File $file
|
||||
*/
|
||||
public function clear() {
|
||||
public function clear()
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->delete('memories');
|
||||
$query->executeStatement();
|
||||
|
|
400
lib/Exif.php
400
lib/Exif.php
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories;
|
||||
|
@ -7,24 +8,15 @@ use OCA\Memories\AppInfo\Application;
|
|||
use OCP\Files\File;
|
||||
use OCP\IConfig;
|
||||
|
||||
class Exif {
|
||||
class Exif
|
||||
{
|
||||
/** Opened instance of exiftool when running in command mode */
|
||||
private static $staticProc = null;
|
||||
private static $staticPipes = null;
|
||||
private static $staticProc;
|
||||
private static $staticPipes;
|
||||
private static $noStaticProc = false;
|
||||
|
||||
/** Initialize static exiftool process for local reads */
|
||||
private static function initializeStaticExiftoolProc() {
|
||||
self::closeStaticExiftoolProc();
|
||||
self::$staticProc = proc_open(['exiftool', '-stay_open', 'true', '-@', '-'], [
|
||||
0 => array('pipe', 'r'),
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
], self::$staticPipes);
|
||||
stream_set_blocking(self::$staticPipes[1], false);
|
||||
}
|
||||
|
||||
public static function closeStaticExiftoolProc() {
|
||||
public static function closeStaticExiftoolProc()
|
||||
{
|
||||
try {
|
||||
if (self::$staticProc) {
|
||||
fclose(self::$staticPipes[0]);
|
||||
|
@ -34,15 +26,18 @@ class Exif {
|
|||
self::$staticProc = null;
|
||||
self::$staticPipes = null;
|
||||
}
|
||||
} catch (\Exception $ex) {}
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
}
|
||||
|
||||
public static function restartStaticExiftoolProc() {
|
||||
public static function restartStaticExiftoolProc()
|
||||
{
|
||||
self::closeStaticExiftoolProc();
|
||||
self::ensureStaticExiftoolProc();
|
||||
}
|
||||
|
||||
public static function ensureStaticExiftoolProc() {
|
||||
public static function ensureStaticExiftoolProc()
|
||||
{
|
||||
if (self::$noStaticProc) {
|
||||
return;
|
||||
}
|
||||
|
@ -50,15 +45,16 @@ class Exif {
|
|||
if (!self::$staticProc) {
|
||||
self::initializeStaticExiftoolProc();
|
||||
usleep(500000); // wait if error
|
||||
if (!proc_get_status(self::$staticProc)["running"]) {
|
||||
error_log("WARN: Failed to create stay_open exiftool process");
|
||||
if (!proc_get_status(self::$staticProc)['running']) {
|
||||
error_log('WARN: Failed to create stay_open exiftool process');
|
||||
self::$noStaticProc = true;
|
||||
self::$staticProc = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!proc_get_status(self::$staticProc)["running"]) {
|
||||
if (!proc_get_status(self::$staticProc)['running']) {
|
||||
self::$staticProc = null;
|
||||
self::ensureStaticExiftoolProc();
|
||||
}
|
||||
|
@ -66,49 +62,51 @@ class Exif {
|
|||
|
||||
/**
|
||||
* Get the path to the user's configured photos directory.
|
||||
* @param IConfig $config
|
||||
* @param string $userId
|
||||
*/
|
||||
public static function getPhotosPath(IConfig &$config, string &$userId) {
|
||||
public static function getPhotosPath(IConfig &$config, string &$userId)
|
||||
{
|
||||
$p = $config->getUserValue($userId, Application::APPNAME, 'timelinePath', '');
|
||||
if (empty($p)) {
|
||||
return 'Photos/';
|
||||
}
|
||||
|
||||
return self::sanitizePath($p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a path to keep only ASCII characters and special characters.
|
||||
* @param string $path
|
||||
*/
|
||||
public static function sanitizePath(string $path) {
|
||||
return mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).\/])", '', $path);
|
||||
public static function sanitizePath(string $path)
|
||||
{
|
||||
return mb_ereg_replace('([^\\w\\s\\d\\-_~,;\\[\\]\\(\\).\\/])', '', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep only one slash if multiple repeating
|
||||
* Keep only one slash if multiple repeating.
|
||||
*/
|
||||
public static function removeExtraSlash(string $path) {
|
||||
public static function removeExtraSlash(string $path)
|
||||
{
|
||||
return mb_ereg_replace('\/\/+', '/', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any leading slash present on the path
|
||||
* Remove any leading slash present on the path.
|
||||
*/
|
||||
public static function removeLeadingSlash(string $path) {
|
||||
public static function removeLeadingSlash(string $path)
|
||||
{
|
||||
return mb_ereg_replace('~^/+~', '', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exif data as a JSON object from a Nextcloud file.
|
||||
* @param File $file
|
||||
*/
|
||||
public static function getExifFromFile(File &$file) {
|
||||
public static function getExifFromFile(File &$file)
|
||||
{
|
||||
// Borrowed from previews
|
||||
// https://github.com/nextcloud/server/blob/19f68b3011a3c040899fb84975a28bd746bddb4b/lib/private/Preview/ProviderV2.php
|
||||
if (!$file->isEncrypted() && $file->getStorage()->isLocal()) {
|
||||
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
|
||||
if (is_string($path)) {
|
||||
if (\is_string($path)) {
|
||||
return self::getExifFromLocalPath($path);
|
||||
}
|
||||
}
|
||||
|
@ -121,96 +119,35 @@ class Exif {
|
|||
|
||||
$exif = self::getExifFromStream($handle);
|
||||
fclose($handle);
|
||||
|
||||
return $exif;
|
||||
}
|
||||
|
||||
/** Get exif data as a JSON object from a local file path */
|
||||
public static function getExifFromLocalPath(string &$path) {
|
||||
if (!is_null(self::$staticProc)) {
|
||||
public static function getExifFromLocalPath(string &$path)
|
||||
{
|
||||
if (null !== self::$staticProc) {
|
||||
self::ensureStaticExiftoolProc();
|
||||
|
||||
return self::getExifFromLocalPathWithStaticProc($path);
|
||||
} else {
|
||||
}
|
||||
|
||||
return self::getExifFromLocalPathWithSeparateProc($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from non blocking handle or throw timeout
|
||||
* @param resource $handle
|
||||
* @param int $timeout milliseconds
|
||||
* @param string $delimiter null for eof
|
||||
*/
|
||||
private static function readOrTimeout($handle, $timeout, $delimiter=null) {
|
||||
$buf = '';
|
||||
$waitedMs = 0;
|
||||
|
||||
while ($waitedMs < $timeout && ($delimiter ? !str_ends_with($buf, $delimiter) : !feof($handle))) {
|
||||
$r = stream_get_contents($handle);
|
||||
if (empty($r)) {
|
||||
$waitedMs++;
|
||||
usleep(1000);
|
||||
continue;
|
||||
}
|
||||
$buf .= $r;
|
||||
}
|
||||
|
||||
if ($waitedMs >= $timeout) {
|
||||
throw new \Exception('Timeout');
|
||||
}
|
||||
|
||||
return $buf;
|
||||
}
|
||||
|
||||
private static function getExifFromLocalPathWithStaticProc(string &$path) {
|
||||
fwrite(self::$staticPipes[0], "$path\n-json\n-api\nQuickTimeUTC=1\n-n\n-execute\n");
|
||||
fflush(self::$staticPipes[0]);
|
||||
|
||||
$readyToken = "\n{ready}\n";
|
||||
|
||||
try {
|
||||
$buf = self::readOrTimeout(self::$staticPipes[1], 5000, $readyToken);
|
||||
$tokPos = strrpos($buf, $readyToken);
|
||||
$buf = substr($buf, 0, $tokPos);
|
||||
return self::processStdout($buf);
|
||||
} catch (\Exception $ex) {
|
||||
error_log("ERROR: Exiftool may have crashed, restarting process [$path]");
|
||||
self::restartStaticExiftoolProc();
|
||||
throw new \Exception("Nothing to read from Exiftool");
|
||||
}
|
||||
}
|
||||
|
||||
private static function getExifFromLocalPathWithSeparateProc(string &$path) {
|
||||
$pipes = [];
|
||||
$proc = proc_open(['exiftool', '-api', 'QuickTimeUTC=1', '-n', '-json', $path], [
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
], $pipes);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
|
||||
try {
|
||||
$stdout = self::readOrTimeout($pipes[1], 5000);
|
||||
return self::processStdout($stdout);
|
||||
} catch (\Exception $ex) {
|
||||
error_log("Exiftool timeout: [$path]");
|
||||
throw new \Exception("Could not read from Exiftool");
|
||||
} finally {
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_terminate($proc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exif data as a JSON object from a stream.
|
||||
*
|
||||
* @param resource $handle
|
||||
*/
|
||||
public static function getExifFromStream(&$handle) {
|
||||
public static function getExifFromStream(&$handle)
|
||||
{
|
||||
// Start exiftool and output to json
|
||||
$pipes = [];
|
||||
$proc = proc_open(['exiftool', '-api', 'QuickTimeUTC=1', '-n', '-json', '-fast', '-'], [
|
||||
0 => array('pipe', 'rb'),
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
0 => ['pipe', 'rb'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
], $pipes);
|
||||
|
||||
// Write the file to exiftool's stdin
|
||||
|
@ -221,12 +158,15 @@ class Exif {
|
|||
|
||||
// Get output from exiftool
|
||||
stream_set_blocking($pipes[1], false);
|
||||
|
||||
try {
|
||||
$stdout = self::readOrTimeout($pipes[1], 5000);
|
||||
|
||||
return self::processStdout($stdout);
|
||||
} catch (\Exception $ex) {
|
||||
error_log("Exiftool timeout for file stream: " . $ex->getMessage());
|
||||
throw new \Exception("Could not read from Exiftool");
|
||||
error_log('Exiftool timeout for file stream: '.$ex->getMessage());
|
||||
|
||||
throw new \Exception('Could not read from Exiftool');
|
||||
} finally {
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
|
@ -234,36 +174,30 @@ class Exif {
|
|||
}
|
||||
}
|
||||
|
||||
/** Get json array from stdout of exiftool */
|
||||
private static function processStdout(string &$stdout) {
|
||||
$json = json_decode($stdout, true);
|
||||
if (!$json) {
|
||||
throw new \Exception('Could not read exif data');
|
||||
}
|
||||
return $json[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse date from exif format and throw error if invalid
|
||||
* Parse date from exif format and throw error if invalid.
|
||||
*
|
||||
* @param string $dt
|
||||
* @param mixed $date
|
||||
*
|
||||
* @return int unix timestamp
|
||||
*/
|
||||
public static function parseExifDate($date) {
|
||||
public static function parseExifDate($date)
|
||||
{
|
||||
$dt = $date;
|
||||
if (isset($dt) && is_string($dt) && !empty($dt)) {
|
||||
if (isset($dt) && \is_string($dt) && !empty($dt)) {
|
||||
$dt = explode('-', explode('+', $dt, 2)[0], 2)[0]; // get rid of timezone if present
|
||||
$dt = \DateTime::createFromFormat('Y:m:d H:i:s', $dt);
|
||||
if (!$dt) {
|
||||
throw new \Exception("Invalid date: $date");
|
||||
throw new \Exception("Invalid date: {$date}");
|
||||
}
|
||||
if ($dt && $dt->getTimestamp() > -5364662400) { // 1800 A.D.
|
||||
return $dt->getTimestamp();
|
||||
} else {
|
||||
throw new \Exception("Date too old: $date");
|
||||
}
|
||||
|
||||
throw new \Exception("Date too old: {$date}");
|
||||
} else {
|
||||
throw new \Exception("No date provided");
|
||||
throw new \Exception('No date provided');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,7 +207,8 @@ class Exif {
|
|||
*
|
||||
* @param int $epoch
|
||||
*/
|
||||
public static function forgetTimezone($epoch) {
|
||||
public static function forgetTimezone($epoch)
|
||||
{
|
||||
$dt = new \DateTime();
|
||||
$dt->setTimestamp($epoch);
|
||||
$tz = getenv('TZ'); // at least works on debian ...
|
||||
|
@ -281,16 +216,17 @@ class Exif {
|
|||
$dt->setTimezone(new \DateTimeZone($tz));
|
||||
}
|
||||
$utc = new \DateTime($dt->format('Y-m-d H:i:s'), new \DateTimeZone('UTC'));
|
||||
|
||||
return $utc->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the date taken from either the file or exif data if available.
|
||||
* @param File $file
|
||||
* @param array $exif
|
||||
*
|
||||
* @return int unix timestamp
|
||||
*/
|
||||
public static function getDateTaken(File &$file, array &$exif) {
|
||||
public static function getDateTaken(File &$file, array &$exif)
|
||||
{
|
||||
$dt = $exif['DateTimeOriginal'] ?? null;
|
||||
if (!isset($dt) || empty($dt)) {
|
||||
$dt = $exif['CreateDate'] ?? null;
|
||||
|
@ -307,25 +243,27 @@ class Exif {
|
|||
$dateTaken = $file->getCreationTime();
|
||||
|
||||
// Fall back to modification time
|
||||
if ($dateTaken == 0) {
|
||||
if (0 === $dateTaken) {
|
||||
$dateTaken = $file->getMtime();
|
||||
}
|
||||
|
||||
return self::forgetTimezone($dateTaken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image dimensions from Exif data
|
||||
* @param array $exif
|
||||
* Get image dimensions from Exif data.
|
||||
*
|
||||
* @return array [width, height]
|
||||
*/
|
||||
public static function getDimensions(array &$exif) {
|
||||
public static function getDimensions(array &$exif)
|
||||
{
|
||||
$width = $exif['ImageWidth'] ?? 0;
|
||||
$height = $exif['ImageHeight'] ?? 0;
|
||||
|
||||
// Check if image is rotated and we need to swap width and height
|
||||
$rotation = $exif['Rotation'] ?? 0;
|
||||
$orientation = $exif['Orientation'] ?? 0;
|
||||
if (in_array($orientation, [5, 6, 7, 8]) || in_array($rotation, [90, 270])) {
|
||||
if (\in_array($orientation, [5, 6, 7, 8], true) || \in_array($rotation, [90, 270], true)) {
|
||||
return [$height, $width];
|
||||
}
|
||||
|
||||
|
@ -333,16 +271,16 @@ class Exif {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update exif date using exiftool
|
||||
* Update exif date using exiftool.
|
||||
*
|
||||
* @param File $file
|
||||
* @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS)
|
||||
*/
|
||||
public static function updateExifDate(File &$file, string $newDate) {
|
||||
public static function updateExifDate(File &$file, string $newDate)
|
||||
{
|
||||
// Check for local files -- this is easier
|
||||
if (!$file->isEncrypted() && $file->getStorage()->isLocal()) {
|
||||
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
|
||||
if (is_string($path)) {
|
||||
if (\is_string($path)) {
|
||||
return self::updateExifDateForLocalFile($path, $newDate);
|
||||
}
|
||||
}
|
||||
|
@ -352,36 +290,12 @@ class Exif {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update exif date using exiftool for a local file
|
||||
* Update exif date for stream.
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS)
|
||||
* @return bool
|
||||
*/
|
||||
private static function updateExifDateForLocalFile(string $path, string $newDate) {
|
||||
$cmd = ['exiftool', '-api', 'QuickTimeUTC=1', '-overwrite_original', '-DateTimeOriginal=' . $newDate, $path];
|
||||
$proc = proc_open($cmd, [
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
], $pipes);
|
||||
$stdout = self::readOrTimeout($pipes[1], 300000);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_terminate($proc);
|
||||
if (strpos($stdout, 'error') !== false) {
|
||||
error_log("Exiftool error: $stdout");
|
||||
throw new \Exception("Could not update exif date: " . $stdout);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update exif date for stream
|
||||
*
|
||||
* @param File $file
|
||||
* @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS)
|
||||
*/
|
||||
public static function updateExifDateForStreamFile(File &$file, string $newDate) {
|
||||
public static function updateExifDateForStreamFile(File &$file, string $newDate)
|
||||
{
|
||||
// Temp file for output, so we can compare sizes before writing to the actual file
|
||||
$tmpfile = tmpfile();
|
||||
|
||||
|
@ -390,11 +304,11 @@ class Exif {
|
|||
$pipes = [];
|
||||
$proc = proc_open([
|
||||
'exiftool', '-api', 'QuickTimeUTC=1',
|
||||
'-overwrite_original', '-DateTimeOriginal=' . $newDate, '-'
|
||||
'-overwrite_original', '-DateTimeOriginal='.$newDate, '-',
|
||||
], [
|
||||
0 => array('pipe', 'rb'),
|
||||
1 => array('pipe', 'w'),
|
||||
2 => array('pipe', 'w'),
|
||||
0 => ['pipe', 'rb'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
], $pipes);
|
||||
|
||||
// Write the file to exiftool's stdin
|
||||
|
@ -418,9 +332,10 @@ class Exif {
|
|||
while ($waitedMs < $timeout && !feof($pipes[1])) {
|
||||
$r = stream_copy_to_stream($pipes[1], $tmpfile, 1024 * 1024);
|
||||
$newLen += $r;
|
||||
if ($r === 0) {
|
||||
$waitedMs++;
|
||||
if (0 === $r) {
|
||||
++$waitedMs;
|
||||
usleep(1000);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -428,8 +343,9 @@ class Exif {
|
|||
throw new \Exception('Timeout');
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
error_log("Exiftool timeout for file stream: " . $ex->getMessage());
|
||||
throw new \Exception("Could not read from Exiftool");
|
||||
error_log('Exiftool timeout for file stream: '.$ex->getMessage());
|
||||
|
||||
throw new \Exception('Could not read from Exiftool');
|
||||
} finally {
|
||||
// Close the pipes
|
||||
fclose($pipes[1]);
|
||||
|
@ -440,8 +356,9 @@ class Exif {
|
|||
// Check the new length of the file
|
||||
// If the new length and old length are more different than 1KB, abort
|
||||
if (abs($newLen - $origLen) > 1024) {
|
||||
error_log("Exiftool error: new length $newLen, old length $origLen");
|
||||
throw new \Exception("Exiftool error: new length $newLen, old length $origLen");
|
||||
error_log("Exiftool error: new length {$newLen}, old length {$origLen}");
|
||||
|
||||
throw new \Exception("Exiftool error: new length {$newLen}, old length {$origLen}");
|
||||
}
|
||||
|
||||
// Write the temp file to the actual file
|
||||
|
@ -451,14 +368,16 @@ class Exif {
|
|||
throw new \Exception('Could not open file for writing');
|
||||
}
|
||||
$wroteBytes = 0;
|
||||
|
||||
try {
|
||||
$wroteBytes = stream_copy_to_stream($tmpfile, $out);
|
||||
} finally {
|
||||
fclose($out);
|
||||
}
|
||||
if ($wroteBytes !== $newLen) {
|
||||
error_log("Exiftool error: wrote $r bytes, expected $newLen");
|
||||
throw new \Exception("Could not write to file");
|
||||
error_log("Exiftool error: wrote {$r} bytes, expected {$newLen}");
|
||||
|
||||
throw new \Exception('Could not write to file');
|
||||
}
|
||||
|
||||
// All done at this point
|
||||
|
@ -468,4 +387,129 @@ class Exif {
|
|||
fclose($tmpfile);
|
||||
}
|
||||
}
|
||||
|
||||
/** Initialize static exiftool process for local reads */
|
||||
private static function initializeStaticExiftoolProc()
|
||||
{
|
||||
self::closeStaticExiftoolProc();
|
||||
self::$staticProc = proc_open(['exiftool', '-stay_open', 'true', '-@', '-'], [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
], self::$staticPipes);
|
||||
stream_set_blocking(self::$staticPipes[1], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from non blocking handle or throw timeout.
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $timeout milliseconds
|
||||
* @param string $delimiter null for eof
|
||||
*/
|
||||
private static function readOrTimeout($handle, $timeout, $delimiter = null)
|
||||
{
|
||||
$buf = '';
|
||||
$waitedMs = 0;
|
||||
|
||||
while ($waitedMs < $timeout && ($delimiter ? !str_ends_with($buf, $delimiter) : !feof($handle))) {
|
||||
$r = stream_get_contents($handle);
|
||||
if (empty($r)) {
|
||||
++$waitedMs;
|
||||
usleep(1000);
|
||||
|
||||
continue;
|
||||
}
|
||||
$buf .= $r;
|
||||
}
|
||||
|
||||
if ($waitedMs >= $timeout) {
|
||||
throw new \Exception('Timeout');
|
||||
}
|
||||
|
||||
return $buf;
|
||||
}
|
||||
|
||||
private static function getExifFromLocalPathWithStaticProc(string &$path)
|
||||
{
|
||||
fwrite(self::$staticPipes[0], "{$path}\n-json\n-api\nQuickTimeUTC=1\n-n\n-execute\n");
|
||||
fflush(self::$staticPipes[0]);
|
||||
|
||||
$readyToken = "\n{ready}\n";
|
||||
|
||||
try {
|
||||
$buf = self::readOrTimeout(self::$staticPipes[1], 5000, $readyToken);
|
||||
$tokPos = strrpos($buf, $readyToken);
|
||||
$buf = substr($buf, 0, $tokPos);
|
||||
|
||||
return self::processStdout($buf);
|
||||
} catch (\Exception $ex) {
|
||||
error_log("ERROR: Exiftool may have crashed, restarting process [{$path}]");
|
||||
self::restartStaticExiftoolProc();
|
||||
|
||||
throw new \Exception('Nothing to read from Exiftool');
|
||||
}
|
||||
}
|
||||
|
||||
private static function getExifFromLocalPathWithSeparateProc(string &$path)
|
||||
{
|
||||
$pipes = [];
|
||||
$proc = proc_open(['exiftool', '-api', 'QuickTimeUTC=1', '-n', '-json', $path], [
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
], $pipes);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
|
||||
try {
|
||||
$stdout = self::readOrTimeout($pipes[1], 5000);
|
||||
|
||||
return self::processStdout($stdout);
|
||||
} catch (\Exception $ex) {
|
||||
error_log("Exiftool timeout: [{$path}]");
|
||||
|
||||
throw new \Exception('Could not read from Exiftool');
|
||||
} finally {
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_terminate($proc);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get json array from stdout of exiftool */
|
||||
private static function processStdout(string &$stdout)
|
||||
{
|
||||
$json = json_decode($stdout, true);
|
||||
if (!$json) {
|
||||
throw new \Exception('Could not read exif data');
|
||||
}
|
||||
|
||||
return $json[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update exif date using exiftool for a local file.
|
||||
*
|
||||
* @param string $newDate formatted in standard Exif format (YYYY:MM:DD HH:MM:SS)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function updateExifDateForLocalFile(string $path, string $newDate)
|
||||
{
|
||||
$cmd = ['exiftool', '-api', 'QuickTimeUTC=1', '-overwrite_original', '-DateTimeOriginal='.$newDate, $path];
|
||||
$proc = proc_open($cmd, [
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
], $pipes);
|
||||
$stdout = self::readOrTimeout($pipes[1], 300000);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_terminate($proc);
|
||||
if (false !== strpos($stdout, 'error')) {
|
||||
error_log("Exiftool error: {$stdout}");
|
||||
|
||||
throw new \Exception('Could not update exif date: '.$stdout);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -18,28 +17,29 @@ declare(strict_types=1);
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Listeners;
|
||||
|
||||
use \OCA\Memories\Db\TimelineWrite;
|
||||
|
||||
use OCA\Memories\Db\TimelineWrite;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\Events\Node\NodeDeletedEvent;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class PostDeleteListener implements IEventListener {
|
||||
class PostDeleteListener implements IEventListener
|
||||
{
|
||||
private TimelineWrite $util;
|
||||
|
||||
public function __construct(IDBConnection $connection) {
|
||||
public function __construct(IDBConnection $connection)
|
||||
{
|
||||
$this->util = new TimelineWrite($connection);
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof NodeDeletedEvent)) {
|
||||
public function handle(Event $event): void
|
||||
{
|
||||
if (!$event instanceof NodeDeletedEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -18,13 +17,11 @@ declare(strict_types=1);
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Listeners;
|
||||
|
||||
use \OCA\Memories\Db\TimelineWrite;
|
||||
|
||||
use OCA\Memories\Db\TimelineWrite;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\Events\Node\NodeTouchedEvent;
|
||||
|
@ -33,18 +30,23 @@ use OCP\Files\Folder;
|
|||
use OCP\IDBConnection;
|
||||
use OCP\IUserManager;
|
||||
|
||||
class PostWriteListener implements IEventListener {
|
||||
class PostWriteListener implements IEventListener
|
||||
{
|
||||
private TimelineWrite $timelineWrite;
|
||||
|
||||
public function __construct(IDBConnection $connection,
|
||||
IUserManager $userManager) {
|
||||
public function __construct(
|
||||
IDBConnection $connection,
|
||||
IUserManager $userManager
|
||||
)
|
||||
{
|
||||
$this->userManager = $userManager;
|
||||
$this->timelineWrite = new TimelineWrite($connection);
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof NodeWrittenEvent) &&
|
||||
!($event instanceof NodeTouchedEvent)) {
|
||||
public function handle(Event $event): void
|
||||
{
|
||||
if (!($event instanceof NodeWrittenEvent)
|
||||
&& !($event instanceof NodeTouchedEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -63,14 +65,14 @@ class PostWriteListener implements IEventListener {
|
|||
// in reverse order from root to leaf. The rationale is that the
|
||||
// .nomedia file is most likely to be in higher level directories.
|
||||
$parents = [];
|
||||
|
||||
try {
|
||||
$parent = $node->getParent();
|
||||
while ($parent) {
|
||||
$parents[] = $parent;
|
||||
$parent = $parent->getParent();
|
||||
}
|
||||
}
|
||||
catch (\OCP\Files\NotFoundException $e) {
|
||||
} catch (\OCP\Files\NotFoundException $e) {
|
||||
// This happens when the parent is in the root directory
|
||||
// and getParent() is called on it.
|
||||
}
|
||||
|
|
|
@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Varun Patil <radialapps@gmail.com>
|
||||
*
|
||||
* @author Varun Patil <radialapps@gmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,26 +19,25 @@ declare(strict_types=1);
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\Types;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version000000Date20220812163631 extends SimpleMigrationStep {
|
||||
|
||||
class Version000000Date20220812163631 extends SimpleMigrationStep
|
||||
{
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
*
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options)
|
||||
{
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
|
@ -66,7 +63,7 @@ class Version000000Date20220812163631 extends SimpleMigrationStep {
|
|||
]);
|
||||
$table->addColumn('isvideo', Types::BOOLEAN, [
|
||||
'notnull' => false,
|
||||
'default' => false
|
||||
'default' => false,
|
||||
]);
|
||||
$table->addColumn('mtime', Types::INTEGER, [
|
||||
'notnull' => true,
|
||||
|
|
|
@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Your name <your@email.com>
|
||||
*
|
||||
* @author Your name <your@email.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,35 +19,34 @@ declare(strict_types=1);
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
/**
|
||||
* Auto-generated migration step: Please modify to your needs!
|
||||
*/
|
||||
class Version200000Date20220924015634 extends SimpleMigrationStep {
|
||||
|
||||
class Version200000Date20220924015634 extends SimpleMigrationStep
|
||||
{
|
||||
/** @var IDBConnection */
|
||||
private $dbc;
|
||||
|
||||
public function __construct(IDBConnection $dbc) {
|
||||
public function __construct(IDBConnection $dbc)
|
||||
{
|
||||
$this->dbc = $dbc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
*/
|
||||
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
|
||||
{
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
if ($schema->hasTable('memories')) {
|
||||
|
@ -62,12 +59,10 @@ class Version200000Date20220924015634 extends SimpleMigrationStep {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper
|
||||
{
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
|
@ -90,10 +85,9 @@ class Version200000Date20220924015634 extends SimpleMigrationStep {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
*/
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Your name <your@email.com>
|
||||
*
|
||||
* @author Your name <your@email.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,37 +19,33 @@ declare(strict_types=1);
|
|||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\Types;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
/**
|
||||
* Auto-generated migration step: Please modify to your needs!
|
||||
*/
|
||||
class Version400000Date20221015121115 extends SimpleMigrationStep {
|
||||
|
||||
class Version400000Date20221015121115 extends SimpleMigrationStep
|
||||
{
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
*/
|
||||
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper
|
||||
{
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
|
@ -74,10 +68,9 @@ class Version400000Date20221015121115 extends SimpleMigrationStep {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
*/
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
10
lib/Util.php
10
lib/Util.php
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories;
|
||||
|
@ -6,7 +7,8 @@ namespace OCA\Memories;
|
|||
use OCA\Memories\AppInfo\Application;
|
||||
use OCP\IConfig;
|
||||
|
||||
class Util {
|
||||
class Util
|
||||
{
|
||||
public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist
|
||||
public static $TAG_DAYID_FOLDERS = -(1 << 30) + 1;
|
||||
|
||||
|
@ -14,14 +16,14 @@ class Util {
|
|||
|
||||
/**
|
||||
* Get the path to the user's configured photos directory.
|
||||
* @param IConfig $config
|
||||
* @param string $userId
|
||||
*/
|
||||
public static function getPhotosPath(IConfig &$config, string $userId) {
|
||||
public static function getPhotosPath(IConfig &$config, string $userId)
|
||||
{
|
||||
$p = $config->getUserValue($userId, Application::APPNAME, 'timelinePath', '');
|
||||
if (empty($p)) {
|
||||
return '/Photos/';
|
||||
}
|
||||
|
||||
return $p;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue