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