542 lines
13 KiB
PHP
Executable File
542 lines
13 KiB
PHP
Executable File
<?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;
|
||
}
|
||
}
|