2022-08-13 01:58:37 +00:00
< ? php
declare ( strict_types = 1 );
/**
* @ copyright Copyright ( c ) 2022 , Varun Patil < radialapps @ gmail . com >
* @ author Varun Patil < radialapps @ gmail . com >
* @ license AGPL - 3.0 - or - later
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < http :// www . gnu . org / licenses />.
*/
2022-08-18 18:27:25 +00:00
namespace OCA\Memories\Command ;
2022-08-13 01:58:37 +00:00
2022-10-19 17:10:36 +00:00
use OCA\Files_External\Service\GlobalStoragesService ;
2022-10-25 00:47:25 +00:00
use OCA\Memories\AppInfo\Application ;
2022-10-19 17:10:36 +00:00
use OCA\Memories\Db\TimelineWrite ;
2022-08-13 01:58:37 +00:00
use OCP\Encryption\IManager ;
use OCP\Files\File ;
use OCP\Files\Folder ;
use OCP\Files\IRootFolder ;
use OCP\Files\StorageNotAvailableException ;
use OCP\IConfig ;
use OCP\IDBConnection ;
use OCP\IPreview ;
use OCP\IUser ;
use OCP\IUserManager ;
use Psr\Container\ContainerExceptionInterface ;
use Psr\Container\ContainerInterface ;
use Symfony\Component\Console\Command\Command ;
use Symfony\Component\Console\Input\InputInterface ;
2022-09-09 15:07:05 +00:00
use Symfony\Component\Console\Input\InputOption ;
2022-08-13 01:58:37 +00:00
use Symfony\Component\Console\Output\OutputInterface ;
2022-10-19 17:10:36 +00:00
class Index extends Command
{
2022-09-09 07:31:42 +00:00
/** @var ?GlobalStoragesService */
protected $globalService ;
/** @var int[][] */
protected array $sizes ;
protected IUserManager $userManager ;
protected IRootFolder $rootFolder ;
2022-10-25 00:47:25 +00:00
protected IPreview $preview ;
2022-09-09 07:31:42 +00:00
protected IConfig $config ;
protected OutputInterface $output ;
protected IManager $encryptionManager ;
protected IDBConnection $connection ;
protected TimelineWrite $timelineWrite ;
// Stats
private int $nProcessed = 0 ;
private int $nSkipped = 0 ;
private int $nInvalid = 0 ;
2022-10-25 17:49:42 +00:00
// Helper for the progress bar
private int $previousLineLength = 0 ;
2022-10-19 17:10:36 +00:00
public function __construct (
IRootFolder $rootFolder ,
IUserManager $userManager ,
2022-10-25 00:47:25 +00:00
IPreview $preview ,
2022-10-19 17:10:36 +00:00
IConfig $config ,
IManager $encryptionManager ,
IDBConnection $connection ,
ContainerInterface $container
2022-10-19 17:15:14 +00:00
) {
2022-09-09 07:31:42 +00:00
parent :: __construct ();
$this -> userManager = $userManager ;
$this -> rootFolder = $rootFolder ;
2022-10-25 00:47:25 +00:00
$this -> preview = $preview ;
2022-09-09 07:31:42 +00:00
$this -> config = $config ;
$this -> encryptionManager = $encryptionManager ;
$this -> connection = $connection ;
2022-10-25 00:47:25 +00:00
$this -> timelineWrite = new TimelineWrite ( $connection , $preview );
2022-09-09 07:31:42 +00:00
try {
$this -> globalService = $container -> get ( GlobalStoragesService :: class );
} catch ( ContainerExceptionInterface $e ) {
$this -> globalService = null ;
}
}
2022-10-19 17:10:36 +00:00
protected function configure () : void
{
2022-09-09 07:31:42 +00:00
$this
-> setName ( 'memories:index' )
2022-09-09 15:07:05 +00:00
-> setDescription ( 'Generate photo entries' )
-> addOption (
'refresh' ,
'f' ,
InputOption :: VALUE_NONE ,
'Refresh existing entries'
2022-09-11 00:15:40 +00:00
)
-> addOption (
'clear' ,
null ,
InputOption :: VALUE_NONE ,
'Clear existing index before creating a new one (SLOW)'
2022-10-19 17:10:36 +00:00
)
;
2022-09-09 07:31:42 +00:00
}
2022-10-19 17:10:36 +00:00
protected function execute ( InputInterface $input , OutputInterface $output ) : int
{
2022-10-25 00:47:25 +00:00
// Print mime type support information
$output -> writeln ( 'MIME Type support:' );
2022-10-25 17:20:50 +00:00
$mimes = array_merge ( Application :: IMAGE_MIMES , Application :: VIDEO_MIMES );
$someUnsupported = false ;
foreach ( $mimes as & $mimeType ) {
2022-10-25 00:47:25 +00:00
if ( $this -> preview -> isMimeSupported ( $mimeType )) {
$output -> writeln ( " { $mimeType } : supported " );
} else {
2022-10-25 17:20:50 +00:00
$output -> writeln ( " { $mimeType } : <error>not supported</error> " );
$someUnsupported = true ;
2022-10-25 00:47:25 +00:00
}
}
2022-10-25 17:20:50 +00:00
// Print file type support info
if ( $someUnsupported ) {
$output -> writeln ( " \n Some file types are not supported by your preview provider. \n Please see https://github.com/pulsejet/memories/wiki/File-Type-Support \n " );
2022-10-25 17:30:17 +00:00
} else {
$output -> writeln ( " \n All file types are supported by your preview provider. \n " );
2022-10-25 17:20:50 +00:00
}
2022-10-25 00:47:25 +00:00
2022-09-09 15:07:05 +00:00
// Get options and arguments
$refresh = $input -> getOption ( 'refresh' ) ? true : false ;
2022-09-11 00:15:40 +00:00
$clear = $input -> getOption ( 'clear' ) ? true : false ;
// Clear index if asked for this
if ( $clear && $input -> isInteractive ()) {
2022-10-19 17:10:36 +00:00
$output -> write ( 'Are you sure you want to clear the existing index? (y/N): ' );
2022-09-11 00:15:40 +00:00
$answer = trim ( fgets ( STDIN ));
2022-10-19 17:10:36 +00:00
if ( 'y' !== $answer ) {
$output -> writeln ( 'Aborting' );
2022-09-11 00:15:40 +00:00
return 1 ;
}
}
if ( $clear ) {
$this -> timelineWrite -> clear ();
2022-10-19 17:10:36 +00:00
$output -> writeln ( 'Cleared existing index' );
2022-09-11 00:15:40 +00:00
}
2022-09-09 15:07:05 +00:00
2022-09-09 15:18:55 +00:00
// Run with the static process
try {
\OCA\Memories\Exif :: ensureStaticExiftoolProc ();
2022-10-19 17:10:36 +00:00
2022-09-09 15:18:55 +00:00
return $this -> executeWithOpts ( $output , $refresh );
} catch ( \Exception $e ) {
2022-10-19 17:10:36 +00:00
error_log ( 'FATAL: ' . $e -> getMessage ());
2022-09-09 15:18:55 +00:00
return 1 ;
} finally {
\OCA\Memories\Exif :: closeStaticExiftoolProc ();
}
}
2022-10-19 17:10:36 +00:00
protected function executeWithOpts ( OutputInterface $output , bool & $refresh ) : int
{
2022-09-09 07:31:42 +00:00
// Refuse to run without exiftool
if ( ! $this -> testExif ()) {
2022-10-20 20:41:34 +00:00
error_log ( 'FATAL: exiftool could not be executed or test failed' );
error_log ( 'Make sure you have perl 5 installed in PATH' );
2022-10-19 17:10:36 +00:00
2022-09-09 15:18:55 +00:00
return 1 ;
2022-09-09 07:31:42 +00:00
}
// Time measurement
$startTime = microtime ( true );
if ( $this -> encryptionManager -> isEnabled ()) {
2022-09-09 15:18:55 +00:00
error_log ( 'FATAL: Encryption is enabled. Aborted.' );
2022-10-19 17:10:36 +00:00
2022-09-09 07:31:42 +00:00
return 1 ;
}
$this -> output = $output ;
2022-08-13 01:58:37 +00:00
2022-09-09 15:07:05 +00:00
$this -> userManager -> callForSeenUsers ( function ( IUser & $user ) use ( & $refresh ) {
$this -> generateUserEntries ( $user , $refresh );
2022-08-13 01:58:37 +00:00
});
2022-09-09 07:31:42 +00:00
// Show some stats
$endTime = microtime ( true );
2022-10-19 17:10:36 +00:00
$execTime = ( int ) (( $endTime - $startTime ) * 1000 ) / 1000 ;
2022-09-09 07:31:42 +00:00
$nTotal = $this -> nInvalid + $this -> nSkipped + $this -> nProcessed ;
2022-10-25 18:48:54 +00:00
$this -> output -> writeln ( '==========================================' );
2022-10-19 17:10:36 +00:00
$this -> output -> writeln ( " Checked { $nTotal } files in { $execTime } sec " );
$this -> output -> writeln ( $this -> nInvalid . ' not valid media items' );
$this -> output -> writeln ( $this -> nSkipped . ' skipped because unmodified' );
$this -> output -> writeln ( $this -> nProcessed . ' (re-)processed' );
$this -> output -> writeln ( '==========================================' );
2022-09-09 07:31:42 +00:00
return 0 ;
}
2022-10-19 17:10:36 +00:00
/** 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
{
2022-09-09 07:31:42 +00:00
\OC_Util :: tearDownFS ();
\OC_Util :: setupFS ( $user -> getUID ());
2022-09-13 17:39:38 +00:00
$uid = $user -> getUID ();
$userFolder = $this -> rootFolder -> getUserFolder ( $uid );
2022-09-24 02:13:49 +00:00
$this -> parseFolder ( $userFolder , $refresh );
2022-10-25 17:49:42 +00:00
if ( $this -> previousLineLength ) {
$this -> output -> write ( " \r " . str_repeat ( ' ' , $this -> previousLineLength ) . " \r " );
}
2022-09-09 07:31:42 +00:00
}
2022-10-19 17:10:36 +00:00
private function parseFolder ( Folder & $folder , bool & $refresh ) : void
{
2022-09-09 07:31:42 +00:00
try {
$folderPath = $folder -> getPath ();
2022-09-11 00:22:05 +00:00
// Respect the '.nomedia' file. If present don't traverse the folder
if ( $folder -> nodeExists ( '.nomedia' )) {
2022-10-25 18:48:54 +00:00
$this -> output -> writeln ( 'Skipping folder ' . $folderPath . ' because of .nomedia file' );
2022-10-25 17:49:42 +00:00
$this -> previousLineLength = 0 ;
2022-10-25 18:48:54 +00:00
2022-09-11 00:22:05 +00:00
return ;
}
2022-10-25 17:49:42 +00:00
// Clear previous line and write new one
$line = 'Scanning folder ' . $folderPath ;
if ( $this -> previousLineLength ) {
$this -> output -> write ( " \r " . str_repeat ( ' ' , $this -> previousLineLength ) . " \r " );
}
$this -> output -> write ( $line . " \r " );
2022-10-25 18:48:54 +00:00
$this -> previousLineLength = \strlen ( $line );
2022-09-09 07:31:42 +00:00
$nodes = $folder -> getDirectoryListing ();
foreach ( $nodes as & $node ) {
if ( $node instanceof Folder ) {
2022-09-24 02:13:49 +00:00
$this -> parseFolder ( $node , $refresh );
2022-09-09 07:31:42 +00:00
} elseif ( $node instanceof File ) {
2022-09-24 02:13:49 +00:00
$this -> parseFile ( $node , $refresh );
2022-09-09 07:31:42 +00:00
}
}
} catch ( StorageNotAvailableException $e ) {
2022-10-19 17:10:36 +00:00
$this -> output -> writeln ( sprintf (
'<error>Storage for folder folder %s is not available: %s</error>' ,
2022-09-09 07:31:42 +00:00
$folder -> getPath (),
$e -> getHint ()
));
}
}
2022-10-19 17:10:36 +00:00
private function parseFile ( File & $file , bool & $refresh ) : void
{
2022-09-24 02:13:49 +00:00
$res = $this -> timelineWrite -> processFile ( $file , $refresh );
2022-10-19 17:10:36 +00:00
if ( 2 === $res ) {
++ $this -> nProcessed ;
} elseif ( 1 === $res ) {
++ $this -> nSkipped ;
2022-09-09 07:31:42 +00:00
} else {
2022-10-19 17:10:36 +00:00
++ $this -> nInvalid ;
2022-09-09 07:31:42 +00:00
}
}
2022-10-19 17:10:36 +00:00
}