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
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