Skip to content

Commit 99fcc1c

Browse files
authored
Merge pull request #361 from thekid/feature/seekable-buffers
Have Buffer class implement the Seekable interface
2 parents bdc979c + 5971b15 commit 99fcc1c

4 files changed

Lines changed: 265 additions & 78 deletions

File tree

src/main/php/io/TempFile.class.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
* printf('Created temporary file "%s"', $f->getURI());
1717
* ```
1818
*
19-
* Note: The temporary file is not deleted when the file
20-
* handle is closed (e.g., a call to close()), this will have
21-
* to be done manually.
19+
* The temporary file is deleted when the object representing
20+
* it goes of out scope and is garbage-collected. To keep the
21+
* file, use the `persistent()` method.
2222
*
2323
* Note: A possible race condition exists: From the time the
2424
* file name string is created (when the constructor is called)
@@ -34,9 +34,23 @@
3434
class TempFile extends File {
3535
private $persistent= false;
3636

37-
/** @param string $prefix default "tmp" */
38-
public function __construct($prefix= 'tmp') {
39-
parent::__construct(tempnam(Environment::tempDir(), $prefix.uniqid(microtime(true))));
37+
/**
38+
* Creates a new temporary file with a given prefix. Uses the environment's
39+
* temporary directory (typically `$TEMP`) if no other location is supplied.
40+
*
41+
* @param string $prefix
42+
* @param ?string|io.Path|io.Folder $location
43+
*/
44+
public function __construct($prefix= 'tmp', $location= null) {
45+
if (null === $location) {
46+
$directory= Environment::tempDir();
47+
} else if ($location instanceof Folder) {
48+
$directory= $location->getURI();
49+
} else {
50+
$directory= (string)$location;
51+
}
52+
53+
parent::__construct(tempnam($directory, $prefix.uniqid(microtime(true))));
4054
}
4155

4256
/**
@@ -75,6 +89,6 @@ public function persistent() {
7589
/** Ensures file is closed and deleted */
7690
public function __destruct() {
7791
parent::__destruct();
78-
$this->persistent || file_exists($this->uri) && unlink($this->uri);
92+
$this->persistent || (file_exists($this->uri) && unlink($this->uri));
7993
}
8094
}

src/main/php/io/streams/Buffer.class.php

Lines changed: 82 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php namespace io\streams;
22

3-
use io\{File, Folder};
4-
use lang\{IllegalArgumentException, IllegalStateException};
3+
use io\{File, Folder, Path, TempFile, IOException};
4+
use lang\IllegalArgumentException;
55

66
/**
77
* Buffers in memory up until a given threshold, using the file system once
@@ -10,28 +10,35 @@
1010
* @see https://github.com/xp-forge/web/issues/118
1111
* @test io.unittest.BufferTest
1212
*/
13-
class Buffer implements InputStream, OutputStream {
14-
private $files, $threshold;
13+
class Buffer implements InputStream, OutputStream, Seekable {
14+
private $files, $threshold, $persist;
1515
private $memory= '';
1616
private $file= null;
1717
private $size= 0;
1818
private $pointer= 0;
19-
private $draining= false;
2019

2120
/**
2221
* Creates a new buffer
2322
*
24-
* @param io.Folder|io.Path|string $files
23+
* @param string|io.Folder|io.Path|io.File $files
2524
* @param int $threshold
25+
* @param bool $persist
2626
* @throws lang.IllegalArgumentException
2727
*/
28-
public function __construct($files, int $threshold) {
28+
public function __construct($files, int $threshold= 0, bool $persist= false) {
2929
if ($threshold < 0) {
3030
throw new IllegalArgumentException('Threshold must be >= 0');
3131
}
32-
33-
$this->files= $files instanceof Folder ? $files->getURI() : (string)$files;
3432
$this->threshold= $threshold;
33+
$this->persist= $persist;
34+
35+
if ($files instanceof File) {
36+
$this->files= fn() => $files;
37+
} else if ($files instanceof Path && $files->isFile()) {
38+
$this->files= fn() => $files->asFile();
39+
} else {
40+
$this->files= fn() => new TempFile("b{$this->threshold}", $files);
41+
}
3542
}
3643

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

43-
/** Returns whether this buffer is draining */
44-
public function draining(): bool { return $this->draining; }
45-
4650
/**
4751
* Write a string
4852
*
49-
* @param var $arg
53+
* @param string $bytes
5054
* @return void
51-
* @throws lang.IllegalStateException
5255
*/
5356
public function write($bytes) {
54-
if ($this->draining) throw new IllegalStateException('Started draining buffer');
55-
56-
$this->size+= strlen($bytes);
57-
if ($this->size <= $this->threshold) {
58-
$this->memory.= $bytes;
59-
return;
60-
}
61-
62-
if (null === $this->file) {
63-
$this->file= new File(tempnam($this->files, "b{$this->threshold}"));
64-
$this->file->open(File::READWRITE);
65-
$this->file->write($this->memory);
66-
$this->memory= null;
57+
$length= strlen($bytes);
58+
59+
if ($this->size + $length <= $this->threshold) {
60+
$tail= strlen($this->memory);
61+
if ($this->pointer < $tail) {
62+
$this->memory= substr_replace($this->memory, $bytes, $this->pointer, $length);
63+
} else if ($this->pointer > $tail) {
64+
$this->memory.= str_repeat("\x00", $this->pointer - $tail).$bytes;
65+
} else {
66+
$this->memory.= $bytes;
67+
}
68+
69+
$this->pointer+= $length;
70+
$this->size= strlen($this->memory);
71+
} else {
72+
if (null === $this->file) {
73+
$this->file= ($this->files)();
74+
$this->file->open(File::REWRITE);
75+
$this->file->write($this->memory);
76+
$this->file->seek($this->pointer, SEEK_SET);
77+
$this->memory= null;
78+
}
79+
80+
$this->file->write($bytes);
81+
$this->size= $this->file->size();
6782
}
68-
$this->file->write($bytes);
6983
}
7084

7185
/** @return void */
7286
public function flush() {
7387
$this->file && $this->file->flush();
7488
}
7589

76-
/**
77-
* Resets buffer to be able to read from the beginning
78-
*
79-
* @return void
80-
*/
81-
public function reset() {
82-
$this->file ? $this->file->seek(0, SEEK_SET) : $this->pointer= 0;
83-
$this->draining= true;
84-
}
85-
8690
/** @return int */
8791
public function available() {
88-
return $this->draining
89-
? $this->size - ($this->file ? $this->file->tell() : $this->pointer)
90-
: $this->size
91-
;
92+
return $this->size - ($this->file ? $this->file->tell() : $this->pointer);
9293
}
9394

9495
/**
@@ -99,22 +100,58 @@ public function available() {
99100
*/
100101
public function read($limit= 8192) {
101102
if ($this->file) {
102-
$this->draining || $this->file->seek(0, SEEK_SET) && $this->draining= true;
103103
return (string)$this->file->read($limit);
104104
} else {
105-
$this->draining= true;
106105
$chunk= substr($this->memory, $this->pointer, $limit);
107106
$this->pointer+= strlen($chunk);
108107
return $chunk;
109108
}
110109
}
111110

111+
/**
112+
* Resets buffer to be able to read from the beginning. Optimized
113+
* form of calling `seek(0, SEEK_SET)`.
114+
*
115+
* @return void
116+
*/
117+
public function reset() {
118+
$this->file ? $this->file->seek(0, SEEK_SET) : $this->pointer= 0;
119+
}
120+
121+
/**
122+
* Seeks to a given offset.
123+
*
124+
* @param int $offset
125+
* @param int $whence SEEK_SET, SEEK_CUR or SEEK_END
126+
* @return void
127+
* @throws io.IOException
128+
*/
129+
public function seek($offset, $whence= SEEK_SET) {
130+
switch ($whence) {
131+
case SEEK_SET: $position= $offset; break;
132+
case SEEK_CUR: $position= ($this->file ? $this->file->tell() : $this->pointer) + $offset; break;
133+
case SEEK_END: $position= $this->size + $offset; break;
134+
default: $position= -1; break;
135+
}
136+
137+
if ($position < 0) {
138+
throw new IOException("Seek error, position {$offset} in mode {$whence}");
139+
}
140+
141+
$this->file ? $this->file->seek($position, SEEK_SET) : $this->pointer= $position;
142+
}
143+
144+
/** @return int */
145+
public function tell() {
146+
return $this->file ? $this->file->tell() : $this->pointer;
147+
}
148+
112149
/** @return void */
113150
public function close() {
114151
if (null === $this->file || !$this->file->isOpen()) return;
115152

116153
$this->file->close();
117-
$this->file->unlink();
154+
$this->persist || ($this->file->exists() && $this->file->unlink());
118155
}
119156

120157
/** Ensure the file (if any) is closed */

0 commit comments

Comments
 (0)