index: allow specifying user and folder (fix #184)

Signed-off-by: Varun Patil <varunpatil@ucla.edu>
pull/563/head
Varun Patil 2023-03-29 16:38:17 -07:00
parent 2c3181b497
commit 70c2b0d11d
2 changed files with 68 additions and 41 deletions

View File

@ -6,6 +6,7 @@ This file is manually updated. Please file an issue if something is missing.
- **Feature**: Use GPS location data for timezone calculation. - **Feature**: Use GPS location data for timezone calculation.
Many cameras do not store the timezone in EXIF data. This feature allows Memories to use the GPS location data to calculate the timezone. To take advantage of this, you will need to run `occ memories:places-setup` followed by `occ memories:index --clear` (or `occ memories:index -f`) to reindex your photos. Many cameras do not store the timezone in EXIF data. This feature allows Memories to use the GPS location data to calculate the timezone. To take advantage of this, you will need to run `occ memories:places-setup` followed by `occ memories:index --clear` (or `occ memories:index -f`) to reindex your photos.
- **Feature**: You can now specify the user and/or folder to index when running `occ memories:index`.
## v4.12.4 + v4.12.5 (2023-03-23) ## v4.12.4 + v4.12.5 (2023-03-23)

View File

@ -47,12 +47,16 @@ class IndexOpts
public bool $refresh = false; public bool $refresh = false;
public bool $clear = false; public bool $clear = false;
public bool $cleanup = false; public bool $cleanup = false;
public ?string $user = null;
public ?string $folder = null;
public function __construct(InputInterface $input) public function __construct(InputInterface $input)
{ {
$this->refresh = (bool) $input->getOption('refresh'); $this->refresh = (bool) $input->getOption('refresh');
$this->clear = (bool) $input->getOption('clear'); $this->clear = (bool) $input->getOption('clear');
$this->cleanup = (bool) $input->getOption('cleanup'); $this->cleanup = (bool) $input->getOption('cleanup');
$this->user = $input->getOption('user');
$this->folder = $input->getOption('folder');
} }
} }
@ -81,6 +85,9 @@ class Index extends Command
// Helper for the progress bar // Helper for the progress bar
private ConsoleSectionOutput $outputSection; private ConsoleSectionOutput $outputSection;
// Command options
private IndexOpts $opts;
public function __construct( public function __construct(
IRootFolder $rootFolder, IRootFolder $rootFolder,
IUserManager $userManager, IUserManager $userManager,
@ -107,24 +114,11 @@ class Index extends Command
$this $this
->setName('memories:index') ->setName('memories:index')
->setDescription('Generate photo entries') ->setDescription('Generate photo entries')
->addOption( ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Index only the specified user')
'refresh', ->addOption('folder', null, InputOption::VALUE_REQUIRED, 'Index only the specified folder')
'f', ->addOption('refresh', 'f', InputOption::VALUE_NONE, 'Refresh existing entries')
InputOption::VALUE_NONE, ->addOption('clear', null, InputOption::VALUE_NONE, 'Clear existing index before creating a new one (slow)')
'Refresh existing entries' ->addOption('cleanup', null, InputOption::VALUE_NONE, 'Remove orphaned entries from index (e.g. from .nomedia files)')
)
->addOption(
'clear',
null,
InputOption::VALUE_NONE,
'Clear existing index before creating a new one (slow)'
)
->addOption(
'cleanup',
null,
InputOption::VALUE_NONE,
'Remove orphaned entries from index (e.g. from .nomedia files)'
)
; ;
} }
@ -138,7 +132,7 @@ class Index extends Command
$output->writeln("\nMIME Type support:"); $output->writeln("\nMIME Type support:");
$mimes = array_merge(Application::IMAGE_MIMES, Application::VIDEO_MIMES); $mimes = array_merge(Application::IMAGE_MIMES, Application::VIDEO_MIMES);
$someUnsupported = false; $someUnsupported = false;
foreach ($mimes as &$mimeType) { foreach ($mimes as $mimeType) {
if ($this->preview->isMimeSupported($mimeType)) { if ($this->preview->isMimeSupported($mimeType)) {
$output->writeln(" {$mimeType}: supported"); $output->writeln(" {$mimeType}: supported");
} else { } else {
@ -155,10 +149,10 @@ class Index extends Command
} }
// Get options and arguments // Get options and arguments
$opts = new IndexOpts($input); $this->opts = new IndexOpts($input);
// Clear index if asked for this // Clear index if asked for this
if ($opts->clear && $input->isInteractive()) { if ($this->opts->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 ('y' !== $answer) { if ('y' !== $answer) {
@ -167,14 +161,21 @@ class Index extends Command
return 1; return 1;
} }
} }
if ($opts->clear) { if ($this->opts->clear) {
$this->timelineWrite->clear(); $this->timelineWrite->clear();
$output->writeln('Cleared existing index'); $output->writeln('Cleared existing index');
} }
// Detect incompatible options
if ($this->opts->cleanup && ($this->opts->user || $this->opts->folder)) {
$output->writeln('<error>Cannot use --cleanup with --user or --folder</error>');
return 1;
}
// Orphan all entries so we can delete them later // Orphan all entries so we can delete them later
// Refresh works similarly, with a different flag on the process call // Refresh works similarly, with a different flag on the process call
if ($opts->cleanup || $opts->refresh) { if ($this->opts->cleanup || $this->opts->refresh) {
$output->write('Marking all entries for refresh / cleanup ... '); $output->write('Marking all entries for refresh / cleanup ... ');
$count = $this->timelineWrite->orphanAll(); $count = $this->timelineWrite->orphanAll();
$output->writeln("{$count} marked"); $output->writeln("{$count} marked");
@ -184,9 +185,9 @@ class Index extends Command
try { try {
\OCA\Memories\Exif::ensureStaticExiftoolProc(); \OCA\Memories\Exif::ensureStaticExiftoolProc();
return $this->executeWithOpts($output, $opts); return $this->executeNow($output);
} catch (\Exception $e) { } catch (\Exception $e) {
error_log('FATAL: '.$e->getMessage()); $this->output->writeln("<error>{$e->getMessage()}</error>");
return 1; return 1;
} finally { } finally {
@ -194,7 +195,7 @@ class Index extends Command
} }
} }
protected function executeWithOpts(OutputInterface $output, IndexOpts &$opts): int protected function executeNow(OutputInterface $output): int
{ {
// Refuse to run without exiftool // Refuse to run without exiftool
if (!$this->testExif()) { if (!$this->testExif()) {
@ -215,13 +216,21 @@ class Index extends Command
} }
$this->output = $output; $this->output = $output;
// Call indexing for each user // Call indexing for specified or each user
$this->userManager->callForSeenUsers(function (IUser &$user) use (&$opts) { if ($uid = $this->opts->user) {
$this->generateUserEntries($user, $opts); if ($user = $this->userManager->get($uid)) {
}); $this->indexOneUser($user);
} else {
throw new \Exception("User {$uid} not found");
}
} else {
$this->userManager->callForSeenUsers(function (IUser $user) {
$this->indexOneUser($user);
});
}
// Clear orphans if asked for this // Clear orphans if asked for this
if ($opts->cleanup || $opts->refresh) { if (($this->opts->cleanup || $this->opts->refresh) && !($this->opts->user || $this->opts->folder)) {
$output->write('Deleting orphaned entries ... '); $output->write('Deleting orphaned entries ... ');
$count = $this->timelineWrite->removeOrphans(); $count = $this->timelineWrite->removeOrphans();
$output->writeln("{$count} deleted"); $output->writeln("{$count} deleted");
@ -278,22 +287,39 @@ class Index extends Command
return true; return true;
} }
private function generateUserEntries(IUser &$user, IndexOpts &$opts): void private function indexOneUser(IUser $user): void
{ {
\OC_Util::tearDownFS(); \OC_Util::tearDownFS();
\OC_Util::setupFS($user->getUID()); \OC_Util::setupFS($user->getUID());
$uid = $user->getUID(); $uid = $user->getUID();
$userFolder = $this->rootFolder->getUserFolder($uid); $folder = $this->rootFolder->getUserFolder($uid);
if ($path = $this->opts->folder) {
try {
$folder = $folder->get($path);
} catch (\OCP\Files\NotFoundException $e) {
$this->output->writeln("<error>Folder {$path} not found for user {$uid}</error>");
return;
}
if (!$folder instanceof Folder) {
$this->output->writeln("<error>Path {$path} is not a folder for user {$uid}</error>");
return;
}
}
$this->outputSection = $this->output->section(); $this->outputSection = $this->output->section();
++$this->nUser; ++$this->nUser;
$this->outputSection->overwrite("Scanning files for {$uid}"); $this->outputSection->overwrite("Scanning files for {$uid}");
$this->parseFolder($userFolder, $opts); $this->indexFolder($folder);
$this->outputSection->overwrite("Scanned all files for {$uid}"); $this->outputSection->overwrite("Scanned all files for {$uid}");
} }
private function parseFolder(Folder &$folder, IndexOpts &$opts): void private function indexFolder(Folder $folder): void
{ {
try { try {
// Respect the '.nomedia' file. If present don't traverse the folder // Respect the '.nomedia' file. If present don't traverse the folder
@ -305,15 +331,15 @@ class Index extends Command
$nodes = $folder->getDirectoryListing(); $nodes = $folder->getDirectoryListing();
foreach ($nodes as $i => &$node) { foreach ($nodes as $i => $node) {
if ($node instanceof Folder) { if ($node instanceof Folder) {
$this->parseFolder($node, $opts); $this->indexFolder($node);
} elseif ($node instanceof File) { } elseif ($node instanceof File) {
$path = $node->getPath(); $path = $node->getPath();
$path = \strlen($path) > 80 ? '...'.substr($path, -77) : $path; $path = \strlen($path) > 80 ? '...'.substr($path, -77) : $path;
$this->outputSection->overwrite("Scanning {$path}"); $this->outputSection->overwrite("Scanning {$path}");
$this->parseFile($node, $opts); $this->indexFile($node);
$this->tempManager->clean(); $this->tempManager->clean();
} }
} }
@ -326,7 +352,7 @@ class Index extends Command
} }
} }
private function parseFile(File &$file, IndexOpts &$opts): void private function indexFile(File $file): void
{ {
// Process the file // Process the file
$res = 1; $res = 1;
@ -335,14 +361,14 @@ class Index extends Command
// If refreshing the index, force reprocessing // If refreshing the index, force reprocessing
// when the file is still an orphan. this way, the // when the file is still an orphan. this way, the
// files are reprocessed exactly once // files are reprocessed exactly once
$force = $opts->refresh ? 2 : 0; $force = $this->opts->refresh ? 2 : 0;
// (re-)process the file // (re-)process the file
$res = $this->timelineWrite->processFile($file, $force); $res = $this->timelineWrite->processFile($file, $force);
// If the file was processed successfully, // If the file was processed successfully,
// remove it from the orphan list // remove it from the orphan list
if ($opts->cleanup || $opts->refresh) { if ($this->opts->cleanup || $this->opts->refresh) {
$this->timelineWrite->unorphan($file); $this->timelineWrite->unorphan($file);
} }
} catch (\Error $e) { } catch (\Error $e) {