401 lines
12 KiB
PHP
401 lines
12 KiB
PHP
|
<?php
|
||
|
namespace nulib\file\base;
|
||
|
|
||
|
use nulib\file\csv\csv_flavours;
|
||
|
use nulib\file\IReader;
|
||
|
use nulib\file\IWriter;
|
||
|
use nulib\NoMoreDataException;
|
||
|
use nulib\os\EOFException;
|
||
|
use nulib\os\IOException;
|
||
|
use nulib\php\iter\AbstractIterator;
|
||
|
use nulib\ref\os\csv\ref_csv;
|
||
|
use nulib\str;
|
||
|
use nulib\ValueException;
|
||
|
|
||
|
/**
|
||
|
* Class Stream: lecture/écriture générique dans un flux
|
||
|
*/
|
||
|
class Stream extends AbstractIterator implements IReader, IWriter {
|
||
|
use TStreamFilter;
|
||
|
|
||
|
/** @var bool les opérations de verrouillages sont-elle activées? */
|
||
|
const USE_LOCKING = false;
|
||
|
|
||
|
/** @var resource */
|
||
|
protected $fd;
|
||
|
|
||
|
/** @var bool */
|
||
|
protected $close;
|
||
|
|
||
|
/** @var bool */
|
||
|
protected $throwOnError;
|
||
|
|
||
|
/** @var bool */
|
||
|
protected $useLocking;
|
||
|
|
||
|
/** @var int nombre d'octets à ignorer au début du fichier lors d'un seek */
|
||
|
protected $seekOffset = 0;
|
||
|
|
||
|
/** @var int|null */
|
||
|
protected $serial;
|
||
|
|
||
|
/** @var array */
|
||
|
protected $stat;
|
||
|
|
||
|
function __construct($fd, bool $close=true, bool $throwOnError=true, ?bool $useLocking=null) {
|
||
|
if ($fd === null) throw ValueException::null("resource");
|
||
|
$this->fd = $fd;
|
||
|
$this->close = $close;
|
||
|
$this->throwOnError = $throwOnError;
|
||
|
if ($useLocking === null) $useLocking = static::USE_LOCKING;
|
||
|
$this->useLocking = $useLocking;
|
||
|
}
|
||
|
|
||
|
#############################################################################
|
||
|
# File
|
||
|
|
||
|
/**
|
||
|
* @return resource|null retourner la resource associée à ce fichier si cela
|
||
|
* a du sens
|
||
|
*/
|
||
|
function getResource() {
|
||
|
IOException::ensure_open($this->fd === null);
|
||
|
$this->_streamAppendFilters($this->fd);
|
||
|
return $this->fd;
|
||
|
}
|
||
|
|
||
|
protected function lock(int $operation, ?int &$wouldBlock=null): bool {
|
||
|
$locked = flock($this->getResource(), $operation, $wouldBlock);
|
||
|
if ($locked) return true;
|
||
|
if ($operation & LOCK_NB) return false;
|
||
|
else throw IOException::error();
|
||
|
}
|
||
|
|
||
|
protected function unlock(bool $close=false): void {
|
||
|
if ($this->fd !== null) {
|
||
|
flock($this->fd, LOCK_UN);
|
||
|
if ($close) $this->close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function isatty(): bool {
|
||
|
return stream_isatty($this->getResource());
|
||
|
}
|
||
|
|
||
|
/** obtenir des informations sur le fichier */
|
||
|
function fstat(bool $reload=false): array {
|
||
|
if ($this->stat === null || $reload) {
|
||
|
$fd = $this->getResource();
|
||
|
$this->stat = IOException::ensure_valid(fstat($fd), $this->throwOnError);
|
||
|
}
|
||
|
return $this->stat;
|
||
|
}
|
||
|
|
||
|
function getSize(?int $seekOffset=null): int {
|
||
|
if ($seekOffset === null) $seekOffset = $this->seekOffset;
|
||
|
return $this->fstat()["size"] - $seekOffset;
|
||
|
}
|
||
|
|
||
|
/** @throws IOException */
|
||
|
function ftell(?int $seekOffset=null): int {
|
||
|
$fd = $this->getResource();
|
||
|
if ($seekOffset === null) $seekOffset = $this->seekOffset;
|
||
|
return IOException::ensure_valid(ftell($fd), $this->throwOnError) - $seekOffset;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return int la position après avoir déplacé le pointeur
|
||
|
* @throws IOException
|
||
|
*/
|
||
|
function fseek(int $offset, int $whence=SEEK_SET, ?int $seekOffset=null): int {
|
||
|
$fd = $this->getResource();
|
||
|
if ($seekOffset === null) $seekOffset = $this->seekOffset;
|
||
|
if ($whence === SEEK_SET) $offset += $seekOffset;
|
||
|
IOException::ensure_valid(fseek($fd, $offset, $whence), $this->throwOnError, -1);
|
||
|
return $this->ftell($seekOffset);
|
||
|
}
|
||
|
|
||
|
function seek(int $offset, int $whence=SEEK_SET): self {
|
||
|
$this->fseek($offset, $whence);
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/** fermer le fichier si c'est nécessaire */
|
||
|
function close(bool $close=true, ?int $ifSerial=null): void {
|
||
|
AbstractIterator::rewind();
|
||
|
if ($this->fd !== null && $close && $this->close && ($ifSerial === null || $this->serial === $ifSerial)) {
|
||
|
fclose($this->fd);
|
||
|
$this->fd = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function copyTo(IWriter $dest, bool $closeWriter=false, bool $closeReader=true): void {
|
||
|
$srcr = $this->getResource();
|
||
|
$destr = $dest->getResource();
|
||
|
if ($srcr !== null && $destr !== null) {
|
||
|
while (!feof($srcr)) {
|
||
|
fwrite($destr, fread($srcr, 8192));
|
||
|
}
|
||
|
} else {
|
||
|
$dest->fwrite($this->getContents(false));
|
||
|
}
|
||
|
if ($closeWriter) $dest->close();
|
||
|
if ($closeReader) $this->close();
|
||
|
}
|
||
|
|
||
|
const DEFAULT_CSV_FLAVOUR = ref_csv::OO_FLAVOUR;
|
||
|
|
||
|
/** @var array paramètres pour la lecture et l'écriture de flux au format CSV */
|
||
|
protected $csvFlavour;
|
||
|
|
||
|
function setCsvFlavour(string $flavour): void {
|
||
|
$this->csvFlavour = csv_flavours::verifix($flavour);
|
||
|
}
|
||
|
|
||
|
protected function getCsvParams($fd): array {
|
||
|
$flavour = $this->csvFlavour;
|
||
|
if ($flavour === null) {
|
||
|
if ($fd === null) {
|
||
|
# utiliser la valeur par défaut
|
||
|
$flavour = static::DEFAULT_CSV_FLAVOUR;
|
||
|
} else {
|
||
|
# il faut déterminer le type de fichier CSV en lisant la première ligne
|
||
|
$pos = IOException::ensure_valid(ftell($fd));
|
||
|
$line = IOException::ensure_valid(fgets($fd));
|
||
|
$line = strpbrk($line, ",;\t");
|
||
|
if ($line === false) {
|
||
|
# aucun séparateur trouvé, prender la valeur par défaut
|
||
|
$flavour = static::DEFAULT_CSV_FLAVOUR;
|
||
|
} else {
|
||
|
$flavour = substr($line, 0, 1);
|
||
|
$flavour = csv_flavours::verifix($flavour);
|
||
|
}
|
||
|
IOException::ensure_valid(fseek($fd, $pos), true, -1);
|
||
|
}
|
||
|
$this->csvFlavour = $flavour;
|
||
|
}
|
||
|
return csv_flavours::get_params($flavour);
|
||
|
}
|
||
|
|
||
|
#############################################################################
|
||
|
# Reader
|
||
|
|
||
|
/** @throws IOException */
|
||
|
function fread(int $length): string {
|
||
|
$fd = $this->getResource();
|
||
|
return IOException::ensure_valid(fread($fd, $length), $this->throwOnError);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* lire la prochaine ligne. la ligne est retournée avec le caractère de fin
|
||
|
* de ligne[\r]\n
|
||
|
*
|
||
|
* @throws EOFException si plus aucune ligne n'est disponible
|
||
|
* @throws IOException si une erreur se produit
|
||
|
*/
|
||
|
function fgets(?int $length=null): string {
|
||
|
$fd = $this->getResource();
|
||
|
if ($length === null) $r = fgets($fd);
|
||
|
else $r = fgets($fd, $length);
|
||
|
return EOFException::ensure_not_eof($r, $this->throwOnError);
|
||
|
}
|
||
|
|
||
|
/** @throws IOException */
|
||
|
function fpassthru(): int {
|
||
|
$fd = $this->getResource();
|
||
|
return IOException::ensure_valid(fpassthru($fd), $this->throwOnError);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* retourner la prochaine ligne au format CSV ou null si le fichier est arrivé
|
||
|
* à sa fin
|
||
|
*/
|
||
|
function fgetcsv(): ?array {
|
||
|
$fd = $this->getResource();
|
||
|
$params = $this->getCsvParams($fd);
|
||
|
$row = fgetcsv($fd, 0, $params[0], $params[1], $params[2]);
|
||
|
if ($row === false && feof($fd)) return null;
|
||
|
return IOException::ensure_valid($row, $this->throwOnError);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* lire la prochaine ligne. la ligne est retournée *sans* le caractère de fin
|
||
|
* de ligne [\r]\n
|
||
|
*
|
||
|
* @throws EOFException si plus aucune ligne n'est disponible
|
||
|
* @throws IOException si une erreur se produit
|
||
|
*/
|
||
|
function readLine(): ?string {
|
||
|
return str::strip_nl($this->fgets());
|
||
|
}
|
||
|
|
||
|
/** lire et retourner toutes les lignes */
|
||
|
function readLines(): array {
|
||
|
return iterator_to_array($this);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* essayer de verrouiller le fichier en lecture. retourner true si l'opération
|
||
|
* réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument
|
||
|
* true
|
||
|
*/
|
||
|
function canRead(): bool {
|
||
|
if ($this->useLocking) return $this->lock(LOCK_SH + LOCK_NB);
|
||
|
else return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* verrouiller en mode partagé puis retourner un objet permettant de lire le
|
||
|
* fichier.
|
||
|
*/
|
||
|
function getReader(bool $lockedByCanRead=false): IReader {
|
||
|
if ($this->useLocking && !$lockedByCanRead) $this->lock(LOCK_SH);
|
||
|
return new class($this->fd, ++$this->serial, $this) extends Stream {
|
||
|
function __construct($fd, int $serial, Stream $parent) {
|
||
|
$this->parent = $parent;
|
||
|
parent::__construct($fd);
|
||
|
}
|
||
|
|
||
|
/** @var Stream */
|
||
|
private $parent;
|
||
|
|
||
|
function close(bool $close=true): void {
|
||
|
if ($this->parent !== null && $close) {
|
||
|
$this->parent->close(true, $this->serial);
|
||
|
$this->fd = null;
|
||
|
$this->parent = null;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/** retourner le contenu du fichier sous forme de chaine */
|
||
|
function getContents(bool $close=true, bool $lockedByCanRead=false): string {
|
||
|
$useLocking = $this->useLocking;
|
||
|
if ($useLocking && !$lockedByCanRead) $this->lock(LOCK_SH);
|
||
|
try {
|
||
|
return IOException::ensure_valid(stream_get_contents($this->fd), $this->throwOnError);
|
||
|
} finally {
|
||
|
if ($useLocking) $this->unlock($close);
|
||
|
elseif ($close) $this->close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function unserialize(?array $options=null, bool $close=true, bool $lockedByCanRead=false) {
|
||
|
$args = [$this->getContents($lockedByCanRead)];
|
||
|
if ($options !== null) $args[] = $options;
|
||
|
return unserialize(...$args);
|
||
|
}
|
||
|
|
||
|
#############################################################################
|
||
|
# Iterator
|
||
|
|
||
|
protected function _setup(): void {
|
||
|
}
|
||
|
|
||
|
protected function _next(&$key) {
|
||
|
try {
|
||
|
return $this->fgets();
|
||
|
} catch (EOFException $e) {
|
||
|
throw new NoMoreDataException();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected function _teardown(): void {
|
||
|
$md = stream_get_meta_data($this->fd);
|
||
|
if ($md["seekable"]) $this->fseek(0);
|
||
|
}
|
||
|
|
||
|
#############################################################################
|
||
|
# Writer
|
||
|
|
||
|
/** @throws IOException */
|
||
|
function fwrite(string $data, ?int $length=null): int {
|
||
|
$fd = $this->getResource();
|
||
|
if ($length === null) $r = fwrite($fd, $data);
|
||
|
else $r = fwrite($fd, $data, $length);
|
||
|
return IOException::ensure_valid($r, $this->throwOnError);
|
||
|
}
|
||
|
|
||
|
function fputcsv(array $row): void {
|
||
|
$fd = $this->getResource();
|
||
|
$params = $this->getCsvParams($fd);
|
||
|
IOException::ensure_valid(fputcsv($fd, $row, $params[0], $params[1], $params[2]));
|
||
|
}
|
||
|
|
||
|
/** @throws IOException */
|
||
|
function fflush(): self {
|
||
|
$fd = $this->getResource();
|
||
|
IOException::ensure_valid(fflush($fd), $this->throwOnError);
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/** @throws IOException */
|
||
|
function ftruncate(int $size): self {
|
||
|
$fd = $this->getResource();
|
||
|
IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError);
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
function writeLines(?iterable $lines): IWriter {
|
||
|
if ($lines !== null) {
|
||
|
foreach ($lines as $line) {
|
||
|
$this->fwrite($line);
|
||
|
$this->fwrite("\n");
|
||
|
}
|
||
|
}
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* essayer de verrouiller le fichier en écriture. retourner true si l'opération
|
||
|
* réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument
|
||
|
* true
|
||
|
*/
|
||
|
function canWrite(): bool {
|
||
|
if ($this->useLocking) return $this->lock(LOCK_EX + LOCK_NB);
|
||
|
else return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* verrouiller en mode exclusif puis retourner un objet permettant d'écrire
|
||
|
* dans le fichier
|
||
|
*/
|
||
|
function getWriter(bool $lockedByCanWrite=false): IWriter {
|
||
|
if ($this->useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX);
|
||
|
return new class($this->fd, ++$this->serial, $this) extends Stream {
|
||
|
function __construct($fd, int $serial, Stream $parent) {
|
||
|
$this->parent = $parent;
|
||
|
$this->serial = $serial;
|
||
|
parent::__construct($fd);
|
||
|
}
|
||
|
|
||
|
/** @var Stream */
|
||
|
private $parent;
|
||
|
|
||
|
function close(bool $close=true): void {
|
||
|
if ($this->parent !== null && $close) {
|
||
|
$this->parent->close(true, $this->serial);
|
||
|
$this->fd = null;
|
||
|
$this->parent = null;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function putContents(string $contents, bool $close=true, bool $lockedByCanWrite=false): void {
|
||
|
$useLocking = $this->useLocking;
|
||
|
if ($useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX);
|
||
|
try {
|
||
|
$this->fwrite($contents);
|
||
|
} finally {
|
||
|
if ($useLocking) $this->unlock($close);
|
||
|
elseif ($close) $this->close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function serialize($object, bool $close=true, bool $lockedByCanWrite=false): void {
|
||
|
$this->putContents(serialize($object), $lockedByCanWrite);
|
||
|
}
|
||
|
}
|