Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/main/php/io/TempFile.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
* printf('Created temporary file "%s"', $f->getURI());
* ```
*
* Note: The temporary file is not deleted when the file
* handle is closed (e.g., a call to close()), this will have
* to be done manually.
* The temporary file is deleted when the object representing
* it goes of out scope and is garbage-collected. To keep the
* file, use the `persistent()` method.
*
* Note: A possible race condition exists: From the time the
* file name string is created (when the constructor is called)
Expand All @@ -34,9 +34,23 @@
class TempFile extends File {
private $persistent= false;

/** @param string $prefix default "tmp" */
public function __construct($prefix= 'tmp') {
parent::__construct(tempnam(Environment::tempDir(), $prefix.uniqid(microtime(true))));
/**
* Creates a new temporary file with a given prefix. Uses the environment's
* temporary directory (typically `$TEMP`) if no other location is supplied.
*
* @param string $prefix
* @param ?string|io.Path|io.Folder $location
*/
public function __construct($prefix= 'tmp', $location= null) {
if (null === $location) {
$directory= Environment::tempDir();
} else if ($location instanceof Folder) {
$directory= $location->getURI();
} else {
$directory= (string)$location;
}

parent::__construct(tempnam($directory, $prefix.uniqid(microtime(true))));
}

/**
Expand Down Expand Up @@ -75,6 +89,6 @@ public function persistent() {
/** Ensures file is closed and deleted */
public function __destruct() {
parent::__destruct();
$this->persistent || file_exists($this->uri) && unlink($this->uri);
$this->persistent || (file_exists($this->uri) && unlink($this->uri));
}
}
127 changes: 82 additions & 45 deletions src/main/php/io/streams/Buffer.class.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php namespace io\streams;

use io\{File, Folder};
use lang\{IllegalArgumentException, IllegalStateException};
use io\{File, Folder, Path, TempFile, IOException};
use lang\IllegalArgumentException;

/**
* Buffers in memory up until a given threshold, using the file system once
Expand All @@ -10,28 +10,35 @@
* @see https://github.com/xp-forge/web/issues/118
* @test io.unittest.BufferTest
*/
class Buffer implements InputStream, OutputStream {
private $files, $threshold;
class Buffer implements InputStream, OutputStream, Seekable {
private $files, $threshold, $persist;
private $memory= '';
private $file= null;
private $size= 0;
private $pointer= 0;
private $draining= false;

/**
* Creates a new buffer
*
* @param io.Folder|io.Path|string $files
* @param string|io.Folder|io.Path|io.File $files
* @param int $threshold
* @param bool $persist
* @throws lang.IllegalArgumentException
*/
public function __construct($files, int $threshold) {
public function __construct($files, int $threshold= 0, bool $persist= false) {
if ($threshold < 0) {
throw new IllegalArgumentException('Threshold must be >= 0');
}

$this->files= $files instanceof Folder ? $files->getURI() : (string)$files;
$this->threshold= $threshold;
$this->persist= $persist;

if ($files instanceof File) {
$this->files= fn() => $files;
} else if ($files instanceof Path && $files->isFile()) {
$this->files= fn() => $files->asFile();
} else {
$this->files= fn() => new TempFile("b{$this->threshold}", $files);
}
}

/** Returns buffer size */
Expand All @@ -40,55 +47,49 @@ public function size(): int { return $this->size; }
/** Returns the underlying file, if any */
public function file(): ?File { return $this->file; }

/** Returns whether this buffer is draining */
public function draining(): bool { return $this->draining; }

/**
* Write a string
*
* @param var $arg
* @param string $bytes
* @return void
* @throws lang.IllegalStateException
*/
public function write($bytes) {
if ($this->draining) throw new IllegalStateException('Started draining buffer');

$this->size+= strlen($bytes);
if ($this->size <= $this->threshold) {
$this->memory.= $bytes;
return;
}

if (null === $this->file) {
$this->file= new File(tempnam($this->files, "b{$this->threshold}"));
$this->file->open(File::READWRITE);
$this->file->write($this->memory);
$this->memory= null;
$length= strlen($bytes);

if ($this->size + $length <= $this->threshold) {
$tail= strlen($this->memory);
if ($this->pointer < $tail) {
$this->memory= substr_replace($this->memory, $bytes, $this->pointer, $length);
} else if ($this->pointer > $tail) {
$this->memory.= str_repeat("\x00", $this->pointer - $tail).$bytes;
} else {
$this->memory.= $bytes;
}

$this->pointer+= $length;
$this->size= strlen($this->memory);
} else {
if (null === $this->file) {
$this->file= ($this->files)();
$this->file->open(File::REWRITE);
$this->file->write($this->memory);
$this->file->seek($this->pointer, SEEK_SET);
$this->memory= null;
}

$this->file->write($bytes);
$this->size= $this->file->size();
}
$this->file->write($bytes);
}

/** @return void */
public function flush() {
$this->file && $this->file->flush();
}

/**
* Resets buffer to be able to read from the beginning
*
* @return void
*/
public function reset() {
$this->file ? $this->file->seek(0, SEEK_SET) : $this->pointer= 0;
$this->draining= true;
}

/** @return int */
public function available() {
return $this->draining
? $this->size - ($this->file ? $this->file->tell() : $this->pointer)
: $this->size
;
return $this->size - ($this->file ? $this->file->tell() : $this->pointer);
}

/**
Expand All @@ -99,22 +100,58 @@ public function available() {
*/
public function read($limit= 8192) {
if ($this->file) {
$this->draining || $this->file->seek(0, SEEK_SET) && $this->draining= true;
return (string)$this->file->read($limit);
} else {
$this->draining= true;
$chunk= substr($this->memory, $this->pointer, $limit);
$this->pointer+= strlen($chunk);
return $chunk;
}
}

/**
* Resets buffer to be able to read from the beginning. Optimized
* form of calling `seek(0, SEEK_SET)`.
*
* @return void
*/
public function reset() {
$this->file ? $this->file->seek(0, SEEK_SET) : $this->pointer= 0;
}

/**
* Seeks to a given offset.
*
* @param int $offset
* @param int $whence SEEK_SET, SEEK_CUR or SEEK_END
* @return void
* @throws io.IOException
*/
public function seek($offset, $whence= SEEK_SET) {
switch ($whence) {
case SEEK_SET: $position= $offset; break;
case SEEK_CUR: $position= ($this->file ? $this->file->tell() : $this->pointer) + $offset; break;
case SEEK_END: $position= $this->size + $offset; break;
default: $position= -1; break;
}

if ($position < 0) {
throw new IOException("Seek error, position {$offset} in mode {$whence}");
}

$this->file ? $this->file->seek($position, SEEK_SET) : $this->pointer= $position;
}

/** @return int */
public function tell() {
return $this->file ? $this->file->tell() : $this->pointer;
}

/** @return void */
public function close() {
if (null === $this->file || !$this->file->isOpen()) return;

$this->file->close();
$this->file->unlink();
$this->persist || ($this->file->exists() && $this->file->unlink());
}

/** Ensure the file (if any) is closed */
Expand Down
Loading
Loading