php-tail 추가

This commit is contained in:
2018-04-01 18:44:45 +09:00
parent 74390602d8
commit 5860a14dae
40 changed files with 1928 additions and 2 deletions
@@ -0,0 +1,32 @@
<?php
namespace PhpExtended\Tail;
/**
* FileNotFoundException class file.
*
* This exception represents a target error when the file was designed because
* the path it represents is not pointing on a file. This exception is also
* thrown if the given path is pointing on a directory or a symlink, or
* anything else that is not a file.
*
* @author Anastaszor
*/
class FileNotFoundException extends TailException
{
/**
* Builds a new FileNotFoundException object.
*
* @param string $filename the name of targeted file
* @param int $nblines the number of lines that were demanded
* @param int $hint an estimation of the line length in that file
*/
public function __construct($filename, $nblines, $hint)
{
parent::__construct($filename, $nblines, $hint,
strtr('The file "{file}" does not exists.', array('{file}' => $filename)),
404
);
}
}
@@ -0,0 +1,32 @@
<?php
namespace PhpExtended\Tail;
/**
* FileTooBigExcepion class file.
*
* This exception represents a try to read on a file that is larger than the
* int max capacity on the system. This exception may occur if the file length
* exceeds the INT32 capacity on windows 32 bits systems. It should not occur
* on 64 bits php (except if the size is larger than 2^63 bytes)
*
* @author Anastaszor
*/
class FileTooBigException extends TailException
{
/**
* Builds a new FileTooBigException object.
*
* @param string $filename the name of targeted file
* @param int $nblines the number of lines that were demanded
* @param int $hint an estimation of the line length in that file
*/
public function __construct($filename, $nblines, $hint)
{
parent::__construct($filename, $nblines, $hint,
strtr('File {filename} is too big to be read.', array('{filename}' => $filename)),
500
);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace PhpExtended\Tail;
/**
* IOException class file.
*
* This exception represents a read error on targeted file that occured during
* the reading process of tail.
*
* @author Anastaszor
*/
class IOException extends TailException
{
/**
* Builds a new IOException object.
*
* @param string $filename the name of targeted file
* @param int $nblines the number of lines that were demanded
* @param int $hint an estimation of the line length in that file
*/
public function __construct($filename, $nblines, $hint)
{
parent::__construct($filename, $nblines, $hint,
strtr('Error in reading file {filename}', array('{filename}' => $filename)),
500
);
}
}
@@ -0,0 +1,30 @@
<?php
namespace PhpExtended\Tail;
/**
* IllegalArgumentException class file.
*
* This exception is thrown when the number of lines that was asked is negative
* or null.
*
* @author Anastaszor
*/
class IllegalArgumentException extends TailException
{
/**
* Builds a new IllegalArgumentException object.
*
* @param string $filename the name of targeted file
* @param int $nblines the number of lines that were demanded
* @param int $hint an estimation of the line length in that file
*/
public function __construct($filename, $nblines, $hint)
{
parent::__construct($filename, $nblines, $hint,
strtr('Error in reading file {filename}', array('{filename}' => $filename)),
500
);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace PhpExtended\Tail;
/**
* IllegalOsException class file.
*
* This exception is thrown when the cheating method to tail a file, i.e.
* calling the tail unix function is choosen, but the running operating system
* is not supporting that function.
*
* @author Anastaszor
*/
class IllegalOsException extends TailException
{
/**
* Builds a new IllegalOsException object.
*
* @param string $filename the name of targeted file
* @param int $nblines the number of lines that were demanded
* @param int $hint an estimation of the line length in that file
*/
public function __construct($filename, $nblines, $hint)
{
parent::__construct($filename, $nblines, $hint,
'You can\'t try to call unix\'s tail function on a non unix system.',
500
);
}
}
@@ -0,0 +1,31 @@
<?php
namespace PhpExtended\Tail;
/**
* OutputBufferException class file.
*
* This exception is thrown when the cheating method is called and something
* went wrong with the output buffers. This usually means that the output
* buffer stack is full, or the ob library is not loaded.
*
* @author Anastaszor
*/
class OutputBufferException extends TailException
{
/**
* Builds a new OutputBufferException object.
*
* @param string $filename the name of targeted file
* @param int $nblines the number of lines that were demanded
* @param int $hint an estimation of the line length in that file
*/
public function __construct($filename, $nblines, $hint)
{
parent::__construct($filename, $nblines, $hint,
"Error when using the output buffer.",
500
);
}
}
+512
View File
@@ -0,0 +1,512 @@
<?php
namespace PhpExtended\Tail;
use PhpExtended\System\OperatingSystem;
/**
* Tail class file.
*
* This class provides methods that tails given file and return an array with
* the wanted final lines of that file.
* This class is the equivalent of a `tail -n X filepath` unix command.
*
* @author Anastaszor
* @see http://stackoverflow.com/questions/15025875/what-is-the-best-way-in-php-to-read-last-lines-from-a-file/15025877
*/
class Tail
{
/**
* If true, the given file should exists. This does not guarantee that
* that file will remain existant for as long as the tailing process will
* need it to be.
*
* @var boolean whether the given file exists.
*/
private $_file_exists = null;
/**
* The path to the file. If it is a relative path, then all the process
* for finding files with fopen() will be used, and this library does not
* modify any of that configuration.
*
* @var string the path of targeted file.
*/
private $_file_path = null;
/**
* Builds a new Tail object on targeted file path. If targeted file path
* does not exists, the object will still be correcly constructed. Check
* the <code>getFileExists()</code> method to be safe as tailing targeted
* file.
*
* @param string $filepath the targeted file path, may be absolute or
* relative, all rules for finding it with fopen will be unchanged.
*/
public function __construct($filepath)
{
$this->_file_path = $filepath;
$this->_file_exists = is_file($this->_file_path);
}
/**
* Gets the file path that was given to this object.
*
* @return string the file path of the target.
*/
public function getFilePath()
{
return $this->_file_path;
}
/**
* Gets whether target file exists or not.
*
* @return boolean
*/
public function getFileExists()
{
return $this->_file_exists;
}
/**
* Gets whether this running system is from an unix family. This is called
* only assuming that ALL linux based operating system have the tail -n
* command available and in their $PATH, so it can direcly be called by
* php's passthru() function.
*
* @return boolean true if `tail -n` is supported.
*/
protected function isUnixSystem()
{
return OperatingSystem::get()->isUnix();
}
/**
* Decides the best way to tail the given file given the environment.
*
* @param int $nblines the number of lines wanted
* @param int $hint a median number of bytes for each line in target file
* @param boolean $silent if false, any error will throw an exception. If
* true, any error will silently return an empty array.
* @return string[] the wanted tailed lines
*/
public function smart($nblines, $hint = null, $silent = false)
{
if(!$this->_file_exists)
{
if($silent) return array();
throw new FileNotFoundException($this->_file_path, $nblines, $hint);
}
$nblines = (int) $nblines;
if($nblines <= 0)
{
if($silent) return array();
throw new IllegalArgumentException($this->_file_path, $nblines, $hint);
}
$filelength = @filesize($this->_file_path);
if($filelength === false)
{
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
if($filelength == 0)
return array();
if($filelength < 0)
{
if($this->isUnixSystem())
return $this->cheat($nblines, $hint, $silent);
if($silent) return array();
throw new FileTooBigException($this->_file_path, $nblines, $hint);
}
if($nblines <= 2)
return $this->single($nblines, $hint, $silent);
if($filelength <= 10240) // 10kB
return $this->naive($nblines, $hint, $silent);
return $this->dynamic($nblines, $hint, $silent);
}
/**
* The naive approach. This will load the whole file into php's buffer
* and extract only the last wanted lines.
*
* @param int $nblines the number of lines wanted
* @param int $hint a median number of bytes for each line in target file
* @param boolean $silent if false, any error will throw an exception. If
* true, any error will silently return an empty array.
* @return string[] the wanted tailed lines
*/
public function naive($nblines, $hint = null, $silent = false)
{
if(!$this->_file_exists)
{
if($silent) return array();
throw new FileNotFoundException($this->_file_path, $nblines, $hint);
}
$nblines = (int) $nblines;
if($nblines <= 0)
{
if($silent) return array();
throw new IllegalArgumentException($this->_file_path, $nblines, $hint);
}
$filedata = file($this->_file_path);
if($filedata === false)
{
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
return array_slice($filedata, -$nblines);
}
/**
* The cheating approach. This will call the tail unix function and get
* the return from it.
*
* @param int $nblines the number of lines wanted
* @param int $hint a median number of bytes for each line in target file
* @param boolean $silent if false, any error will throw an exception. If
* true, any error will silently return an empty array.
* @return string[] the wanted tailed lines
*/
public function cheat($nblines, $hint = null, $silent = false)
{
if(!$this->_file_exists)
{
if($silent) return array();
throw new FileNotFoundException($this->_file_path, $nblines, $hint);
}
$nblines = (int) $nblines;
if($nblines <= 0)
{
if($silent) return array();
throw new IllegalArgumentException($this->_file_path, $nblines, $hint);
}
if(!$this->isUnixSystem())
{
if($silent) return array();
throw new IllegalOsException($this->_file_path, $nblines, $hint);
}
if(!function_exists('ob_start'))
{
if($silent) return array();
throw new OutputBufferException($this->_file_path, $nblines, $hint);
}
if(!ob_start())
{
if($silent) return array();
throw new OutputBufferException($this->_file_path, $nblines, $hint);
}
$returnvar = 0;
passthru('tail -n '.$nblines.' '.escapeshellarg($this->_file_path), $returnvar);
if($returnvar !== 0)
{
ob_end_clean();
if($silent) return array();
throw new TailShellException($this->_file_path, $nblines, $hint, $returnvar);
}
$results = ob_get_clean();
if($results === false)
{
if($silent) return array();
throw new OutputBufferException($this->_file_path, $nblines, $hint);
}
return array_map('trim', explode("\n", $results));
}
/**
* The single byte buffer approach. This will read backward from the end
* of the file each char until wanted lines are read.
*
* @param int $nblines the number of lines wanted
* @param int $hint a median number of bytes for each line in target file
* @param boolean $silent if false, any error will throw an exception. If
* true, any error will silently return an empty array.
* @return string[] the wanted tailed lines
*/
public function single($nblines, $hint = null, $silent = false)
{
if(!$this->_file_exists)
{
if($silent) return array();
throw new FileNotFoundException($nblines, $hint);
}
$nblines = (int) $nblines;
if($nblines <= 0)
{
if($silent) return array();
throw new IllegalArgumentException($this->_file_path, $nblines, $hint);
}
$handle = fopen($this->_file_path, 'r');
if($handle === false)
{
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$linecounter = $nblines;
$pos = -2;
$beginning = false;
$text = array();
while($linecounter > 0)
{
$t = " ";
while($t != "\n")
{
$seek = fseek($handle, $pos, SEEK_END);
if($seek === -1)
{
$beginning = true;
break;
}
$t = fgetc($handle);
if($t === false)
{
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$pos--;
}
$linecounter--;
if($beginning)
{
$rewind = rewind($handle);
if($rewind === false)
{
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
}
$data = fgetc($handle);
if($data === false)
{
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$text[$nblines - $linecounter - 1] = $data;
if($beginning) break;
}
fclose($handle);
return array_reverse($text);
}
/**
* The simple buffer approach. This will read backward from the end of the
* file with a fixed size buffer until wanted lines are read.
*
* @param int $nblines the number of lines wanted
* @param int $hint a median number of bytes for each line in target file
* @param boolean $silent if false, any error will throw an exception. If
* true, any error will silently return an empty array.
* @return string[] the wanted tailed lines
*/
public function simple($nblines, $hint = null, $silent = false)
{
if(!$this->_file_exists)
{
if($silent) return array();
throw new FileNotFoundException($nblines, $hint);
}
$nblines = (int) $nblines;
if($nblines <= 0)
{
if($silent) return array();
throw new IllegalArgumentException($this->_file_path, $nblines, $hint);
}
if($hint === null)
{
// just assuming that this is text file with 40 char per line as a
// median basis. May be larger if it is a log file
$hint = 40;
if(strrpos($this->_file_path, '.log') === strlen($this->_file_path) - 4)
{
$hint = 240;
}
}
$buffer = 4096;
return $this->find($nblines, $buffer, $hint, $silent);
}
/**
* The dynamic buffer approach. This will read backward from the end of the
* file with a fixed size buffer which is proportional to the size of the
* wanted chunk of that file.
*
* @param int $nblines the number of lines wanted
* @param int $hint a median number of bytes for each line in target file
* @param boolean $silent if false, any error will throw an exception. If
* true, any error will silently return an empty array.
* @return string[] the wanted tailed lines
*/
public function dynamic($nblines, $hint = null, $silent = false)
{
if(!$this->_file_exists)
{
if($silent) return array();
throw new FileNotFoundException($nblines, $hint);
}
$nblines = (int) $nblines;
if($nblines <= 0)
{
if($silent) return array();
throw new IllegalArgumentException($this->_file_path, $nblines, $hint);
}
if($hint === null)
{
// just assuming that this is text file with 40 char per line as a
// median basis. May be larger if it is a log file
$hint = 40;
if(strrpos($this->_file_path, '.log') === strlen($this->_file_path) - 4)
{
$hint = 240;
}
}
$buffer = $hint * $nblines / 10;
return $this->find($nblines, $buffer, $hint, $silent);
}
/**
* Extracts the wanted number of lines according to the given buffer
* length.
*
* @param int $nblines the number of lines wanted
* @param int $buffer the size of the buffer that will be used to seek
* @param int $hint an estimation of the median line length on this file
* @param boolean $silent if false, any error will throw an exception.
* If true, any error will silently return an empty array.
* @return string[] the wanted tailed lines
* @throws IOException if any error occur
*/
protected function find($nblines, $buffer, $hint, $silent)
{
$lines = $nblines;
$f = fopen($this->_file_path, 'r');
if($f === false)
{
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$seek = fseek($f, -1, SEEK_END);
if($seek === -1)
{
fclose($f);
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$read = fread($f, 1);
if($read === false)
{
fclose($f);
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
if($read === "\n") $lines -= 1;
// faster initialisation
$seek = min(ftell($f), $hint * $nblines);
$seeked = fseek($f, -$seek, SEEK_CUR);
if($seeked === -1)
{
fclose($f);
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$chunk = fread($f, $seek);
if($chunk === false)
{
fclose($f);
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$output = $chunk;
$seeked = fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
if($seeked === -1)
{
fclose($f);
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$lines -= substr_count($chunk, "\n");
// while we want more
while(ftell($f) > 0 && $lines >= 0)
{
// figure out how far back we should jump
$seek = min(ftell($f), $buffer);
// do the jump backwards relative to where we are
$seeked = fseek($f, -$seek, SEEK_CUR);
if($seeked === -1)
{
fclose($f);
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
// read a chunk and prepend it to out output
$chunk = fread($f, $seek);
if($chunk === false)
{
fclose($f);
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
$output = $chunk . $output;
// jump back to where we started reading
$seeked = fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
if($seeked === -1)
{
fclose($f);
if($silent) return array();
throw new IOException($this->_file_path, $nblines, $hint);
}
// decrease out line counter
$lines -= substr_count($chunk, "\n");
}
fclose($f);
// while we have too many lines
// because the buffer size we might have read too many
while($lines++ < 0)
{
$output = substr($output, strpos($output, "\n") + 1);
}
return explode("\n", str_replace(array("\r\n", "\r"), array("\n", ''), trim($output)));
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace PhpExtended\Tail;
/**
* TailException class file.
*
* This is a generic purpose exception for all of that may occur when tailing
* a file with this library.
*
* @author Anastaszor
*/
class TailException extends \Exception
{
/**
* The path of the file that was given.
*
* @var string the path of the file.
*/
private $_filename = null;
/**
* The number of lines that were asked.
*
* @var int the number of lines.
*/
private $_nblines = null;
/**
* An estimation of the number of characters per line.
*
* @var int the median number of characters per line.
*/
private $_hint = null;
/**
* Builds a new TailException object.
*
* @param string $filename the name of the file
* @param int $nblines the number of lines wanted
* @param int $hint an estimation of the median number of characters per line
* @param string $message the message of the exception
* @param int $code the error code associated to this exception
*/
public function __construct($filename, $nblines, $hint, $message, $code)
{
parent::__construct($message, $code);
$this->_filename = $filename;
$this->_nblines = $nblines;
$this->_hint = $hint;
}
/**
* Gets the file path that was asked.
*
* @return string
*/
public function getFilename()
{
return $this->_filename;
}
/**
* Gets the number of lines that was asked.
*
* @return int
*/
public function getNblines()
{
return $this->_nblines;
}
/**
* Gets the median number of characters per line that was given, or
* calculated, if applicable.
*
* @return int
*/
public function getHint()
{
return $this->_hint;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace PhpExtended\Tail;
/**
* TailShellException class file.
*
* This exception occurred if the `tail -n` command on an unix shell did not
* return the expected result.
*
* @author Anastaszor
*/
class TailShellException extends TailException
{
/**
* Builds a new TailShellException object.
*
* @param string $filename the name of targeted file
* @param int $nblines the number of lines that were demanded
* @param int $hint an estimation of the line length in that file
*/
public function __construct($filename, $nblines, $hint)
{
parent::__construct($filename, $nblines, $hint,
strtr('Error in reading file {filename}', array('{filename}' => $filename)),
500
);
}
}