tikiwiki/packages/tiki-pkg-mediaalchemyst/alchemy/phpexiftool/lib/PHPExiftool/Reader.php

542 lines
13 KiB
PHP
Raw Normal View History

2023-11-20 21:52:04 +01:00
<?php
/**
* This file is part of the PHPExiftool package.
*
* (c) Alchemy <support@alchemy.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPExiftool;
use Doctrine\Common\Collections\ArrayCollection;
use PHPExiftool\Exception\EmptyCollectionException;
use PHPExiftool\Exception\LogicException;
use PHPExiftool\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
/**
*
* Exiftool Reader, inspired by Symfony2 Finder.
*
* It scans files and directories, and provide an iterator on the FileEntities
* generated based on the results.
*
* Example usage:
*
* $Reader = new Reader();
*
* $Reader->in('/path/to/directory')
* ->exclude('tests')
* ->extensions(array('jpg', 'xml));
*
* //Throws an exception if no file found
* $first = $Reader->first();
*
* //Returns null if no file found
* $first = $Reader->getOneOrNull();
*
* foreach($Reader as $entity)
* {
* //Do your logic with FileEntity
* }
*
*
* @todo implement match conditions (-if EXPR) (name or metadata tag)
* @todo implement match filter
* @todo implement sort
* @todo implement -l
*
* @author Romain Neutron <imprec@gmail.com>
*/
class Reader implements \IteratorAggregate
{
protected $files = array();
protected $dirs = array();
protected $excludeDirs = array();
protected $extensions = array();
protected $extensionsToggle = null;
protected $followSymLinks = false;
protected $recursive = true;
protected $ignoreDotFile = false;
protected $sort = array();
protected $parser;
protected $exiftool;
protected $timeout = 60;
/**
*
* @var ArrayCollection
*/
protected $collection;
protected $readers = array();
/**
* Constructor
*/
public function __construct(Exiftool $exiftool, RDFParser $parser)
{
$this->exiftool = $exiftool;
$this->parser = $parser;
}
public function __destruct()
{
$this->parser = null;
$this->collection = null;
}
public function setTimeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
public function reset()
{
$this->files
= $this->dirs
= $this->excludeDirs
= $this->extensions
= $this->sort
= $this->readers = array();
$this->recursive = true;
$this->ignoreDotFile = $this->followSymLinks = false;
$this->extensionsToggle = null;
return $this;
}
/**
* Implements \IteratorAggregate Interface
*
* @return \Iterator
*/
public function getIterator()
{
return $this->all()->getIterator();
}
/**
* Add files to scan
*
* Example usage:
*
* // Will scan 3 files : dc00.jpg in CWD and absolute
* // paths /tmp/image.jpg and /tmp/raw.CR2
* $Reader ->files('dc00.jpg')
* ->files(array('/tmp/image.jpg', '/tmp/raw.CR2'))
*
* @param string|array $files The files
* @return Reader
*/
public function files($files)
{
$this->resetResults();
$this->files = array_merge($this->files, (array) $files);
return $this;
}
/**
* Add dirs to scan
*
* Example usage:
*
* // Will scan 3 dirs : documents in CWD and absolute
* // paths /usr and /var
* $Reader ->in('documents')
* ->in(array('/tmp', '/var'))
*
* @param string|array $dirs The directories
* @return Reader
*/
public function in($dirs)
{
$this->resetResults();
$this->dirs = array_merge($this->dirs, (array) $dirs);
return $this;
}
/**
* Append a reader to this one.
* Finale result will be the sum of the current reader and all appended ones.
*
* @param Reader $reader The reader to append
* @return Reader
*/
public function append(Reader $reader)
{
$this->resetResults();
$this->readers[] = $reader;
return $this;
}
/**
* Sort results with one or many criteria
*
* Example usage:
*
* // Will sort by directory then filename
* $Reader ->in('documents')
* ->sort(array('directory', 'filename'))
*
* // Will sort by filename
* $Reader ->in('documents')
* ->sort('filename')
*
* @param string|array $by
* @return Reader
*/
public function sort($by)
{
static $availableSorts = array(
'directory', 'filename', 'createdate', 'modifydate', 'filesize'
);
foreach ((array) $by as $sort) {
if ( ! in_array($sort, $availableSorts)) {
continue;
}
$this->sort[] = $sort;
}
return $this;
}
/**
* Exclude directories from scan
*
* Warning: only first depth directories can be excluded
* Imagine a directory structure like below, With a scan in "root", only
* sub1 or sub2 can be excluded, not subsub.
*
* root
* ├── sub1
* └── sub2
*    └── subsub
*
* Example usage:
*
* // Will scan documents recursively, discarding documents/test
* $Reader ->in('documents')
* ->exclude(array('test'))
*
* @param string|array $dirs The directories
* @return Reader
*/
public function exclude($dirs)
{
$this->resetResults();
$this->excludeDirs = array_merge($this->excludeDirs, (array) $dirs);
return $this;
}
/**
* Restrict / Discard files based on extensions
* Extensions are case insensitive
*
* @param string|array $extensions The list of extension
* @param Boolean $restrict Toggle restrict/discard method
* @return Reader
* @throws LogicException
*/
public function extensions($extensions, $restrict = true)
{
$this->resetResults();
if ( ! is_null($this->extensionsToggle)) {
if ((boolean) $restrict !== $this->extensionsToggle) {
throw new LogicException('You cannot restrict extensions AND exclude extension at the same time');
}
}
$this->extensionsToggle = (boolean) $restrict;
$this->extensions = array_merge($this->extensions, (array) $extensions);
return $this;
}
/**
* Toggle to enable follow Symbolic Links
*
* @return Reader
*/
public function followSymLinks()
{
$this->resetResults();
$this->followSymLinks = true;
return $this;
}
/**
* Ignore files starting with a dot (.)
*
* Folders starting with a dot are always exluded due to exiftool behaviour.
* You should include them manually
*
* @return Reader
*/
public function ignoreDotFiles()
{
$this->resetResults();
$this->ignoreDotFile = true;
return $this;
}
/**
* Disable recursivity in directories scan.
* If you only specify files, this toggle has no effect
*
* @return Reader
*/
public function notRecursive()
{
$this->resetResults();
$this->recursive = false;
return $this;
}
/**
* Return the first result. If no result available, null is returned
*
* @return FileEntity
*/
public function getOneOrNull()
{
return count($this->all()) === 0 ? null : $this->all()->first();
}
/**
* Return the first result. If no result available, throws an exception
*
* @return FileEntity
* @throws EmptyCollectionException
*/
public function first()
{
if (count($this->all()) === 0) {
throw new EmptyCollectionException('Collection is empty');
}
return $this->all()->first();
}
/**
* Perform the scan and returns all the results
*
* @return ArrayCollection
*/
public function all()
{
if (! $this->collection) {
$this->collection = $this->buildQueryAndExecute();
}
if ($this->readers) {
$elements = $this->collection->toArray();
$this->collection = null;
foreach ($this->readers as $reader) {
$elements = array_merge($elements, $reader->all()->toArray());
}
$this->collection = new ArrayCollection($elements);
}
return $this->collection;
}
public static function create(LoggerInterface $logger)
{
return new static(new Exiftool($logger), new RDFParser());
}
/**
* Reset any computed result
*
* @return Reader
*/
protected function resetResults()
{
$this->collection = null;
return $this;
}
/**
* Build the command returns an ArrayCollection of FileEntity
*
* @return ArrayCollection
*/
protected function buildQueryAndExecute()
{
$result = '';
try {
$result = trim($this->exiftool->executeCommand($this->buildQuery(), $this->timeout));
} catch (RuntimeException $e) {
/**
* In case no file found, an exit code 1 is returned
*/
if (! $this->ignoreDotFile) {
throw $e;
}
}
if ($result === '') {
return new ArrayCollection();
}
$this->parser->open($result);
return $this->parser->ParseEntities();
}
/**
* Compute raw exclude rules to simple ones, based on exclude dirs and search dirs
*
* @param string $rawExcludeDirs
* @param string $rawDirs
* @return array
* @throws RuntimeException
*/
protected function computeExcludeDirs($rawExcludeDirs, $rawSearchDirs)
{
$excludeDirs = array();
foreach ($rawExcludeDirs as $excludeDir) {
$found = false;
/**
* is this a relative path ?
*/
foreach ($rawSearchDirs as $dir) {
$currentPrefix = realpath($dir) . DIRECTORY_SEPARATOR;
$supposedExcluded = str_replace($currentPrefix, '', realpath($currentPrefix . $excludeDir));
if (! $supposedExcluded) {
continue;
}
if (strpos($supposedExcluded, DIRECTORY_SEPARATOR) === false) {
$excludeDirs[] = $supposedExcluded;
$found = true;
break;
}
}
if ($found) {
continue;
}
/**
* is this an absolute path ?
*/
$supposedExcluded = realpath($excludeDir);
if ($supposedExcluded) {
foreach ($rawSearchDirs as $dir) {
$searchDir = realpath($dir) . DIRECTORY_SEPARATOR;
$supposedRelative = str_replace($searchDir, '', $supposedExcluded);
if (strpos($supposedRelative, DIRECTORY_SEPARATOR) !== false) {
continue;
}
if (strpos($supposedExcluded, $searchDir) !== 0) {
continue;
}
if ( ! trim($supposedRelative)) {
continue;
}
$excludeDirs[] = $supposedRelative;
$found = true;
break;
}
}
if (! $found) {
throw new RuntimeException(sprintf("Invalid exclude dir %s ; Exclude dir is limited to the name of a directory at first depth", $excludeDir));
}
}
return $excludeDirs;
}
/**
* Build query from criterias
*
* @return string
*
* @throws LogicException
*/
protected function buildQuery()
{
if (! $this->dirs && ! $this->files) {
throw new LogicException('You have not set any files or directory');
}
$command = '-n -q -b -X -charset UTF8';
if ($this->recursive) {
$command .= ' -r';
}
if (!empty($this->extensions)) {
if (! $this->extensionsToggle) {
$extensionPrefix = ' --ext';
} else {
$extensionPrefix = ' -ext';
}
foreach ($this->extensions as $extension) {
$command .= $extensionPrefix . ' ' . escapeshellarg($extension);
}
}
if (! $this->followSymLinks) {
$command .= ' -i SYMLINKS';
}
if ($this->ignoreDotFile) {
$command .= " -if '\$filename !~ /^\./'";
}
foreach ($this->sort as $sort) {
$command .= ' -fileOrder ' . $sort;
}
foreach ($this->computeExcludeDirs($this->excludeDirs, $this->dirs) as $excludedDir) {
$command .= ' -i ' . escapeshellarg($excludedDir);
}
foreach ($this->dirs as $dir) {
$command .= ' ' . escapeshellarg(realpath($dir));
}
foreach ($this->files as $file) {
$command .= ' ' . escapeshellarg(realpath($file));
}
return $command;
}
}