1 <?php
2
3 namespace Alo\FileSystem;
4
5 use Alo\Exception\FileSystemException as FE;
6
7 if(!defined('GEN_START')) {
8 http_response_code(404);
9 die();
10 }
11
12 /**
13 * Object-oriented file handler
14 *
15 * @author Arturas Molcanovas <a.molcanovas@gmail.com>
16 */
17 class File extends AbstractFileSystem {
18
19 /**
20 * Open for reading only; place the file pointer at the beginning of the file.
21 *
22 * @var string
23 */
24 const M_READ_EXISTING_BEGIN = 'r';
25
26 /**
27 * Open for reading and writing; place the file pointer at the beginning of the file.
28 *
29 * @var string
30 */
31 const M_RW_EXISTING_BEGIN = 'r+';
32
33 /**
34 * Open for writing only; place the file pointer at the beginning of the file
35 * and truncate the file to zero length. If the file does not exist, attempt
36 * to create it.
37 *
38 * @var string
39 */
40 const M_WRITE_TRUNCATE_BEGIN = 'w';
41
42 /**
43 * Open for reading and writing; place the file pointer at the beginning of
44 * the file and truncate the file to zero length. If the file does not exist,
45 * attempt to create it.
46 *
47 * @var string
48 */
49 const M_RW_TRUNCATE_BEGIN = 'w+';
50
51 /**
52 * Open for writing only; place the file pointer at the end of the file. If
53 * the file does not exist, attempt to create it.
54 *
55 * @var string
56 */
57 const M_WRITE_END = 'a';
58
59 /**
60 * Open for reading and writing; place the file pointer at the end of the file.
61 * If the file does not exist, attempt to create it.
62 *
63 * @var string
64 */
65 const M_RW_END = 'a+';
66
67 /**
68 * Create and open for writing only; place the file pointer at the beginning
69 * of the file. If the file already exists, the fopen() call will fail by
70 * returning FALSE and generating an error of level E_WARNING. If the file
71 * does not exist, attempt to create it. This is equivalent to specifying
72 * O_EXCL|O_CREAT flags for the underlying open(2) system call.
73 *
74 * @var string
75 */
76 const M_WRITE_NONEXIST_BEGIN = 'x';
77
78 /**
79 * Create and open for reading and writing; otherwise it has the same behavior as
80 * M_WRITE_NONEXIST_BEGIN
81 *
82 * @var string
83 * @see self::M_WRITE_NONEXIST_BEGIN
84 */
85 const M_RW_NONEXIST_BEGIN = 'x+';
86
87 /**
88 * Open the file for writing only. If the file does not exist, it is created.
89 * If it exists, it is neither truncated (as opposed to M_WRITE_TRUNCATE_BEGIN), nor the call to
90 * this function fails (as is the case with M_WRITE_NONEXIST_BEGIN). The file pointer is positioned
91 * on the beginning of the file. This may be useful if it's desired to get an
92 * advisory lock (see flock()) before attempting to modify the file, as using
93 * M_WRITE_NONEXIST_BEGIN could truncate the file before the lock was
94 * obtained (if truncation is desired, ftruncate() can be used after the lock
95 * is requested).
96 *
97 * @var string
98 * @see self::M_WRITE_NONEXIST_BEGIN
99 * @see self::M_WRITE_TRUNCATE_BEGIN
100 */
101 const M_WRITE_BEGIN = 'c';
102
103 /**
104 * Open the file for reading and writing; otherwise it has the same behavior as
105 * M_WRITE_BEGIN.
106 *
107 * @var string
108 * @see self::M_WRITE_BEGIN;
109 */
110 const M_RW_BEGIN = 'c+';
111
112 /**
113 * Whether GZIP is installed
114 *
115 * @var boolean
116 */
117 protected static $gz;
118
119 /**
120 * The file content
121 *
122 * @var string
123 */
124 protected $content;
125
126 /**
127 * The file name
128 *
129 * @var string
130 */
131 protected $name;
132
133 /**
134 * The file directory
135 *
136 * @var string
137 */
138 protected $dir;
139
140 /**
141 * The full file path. Updates with every setName & setDir
142 *
143 * @var string
144 * @see self::setName()
145 * @see self::setDir()
146 */
147 protected $filepath;
148
149 /**
150 * Instantiates the class
151 *
152 * @author Art <a.molcanovas@gmail.com>
153 */
154 function __construct() {
155 parent::__construct();
156
157 $this->dir = DIR_TMP;
158 self::$gz = function_exists('gzencode');
159 }
160
161 /**
162 * Instantiates the class
163 *
164 * @author Art <a.molcanovas@gmail.com>
165 *
166 * @return File
167 */
168 static function File() {
169 return new File();
170 }
171
172 /**
173 * Converts a filesize for display
174 *
175 * @author Art <a.molcanovas@gmail.com>
176 *
177 * @param int $size The file size in bytes
178 *
179 * @return string The file size in its largest form, e.g. 1024 bytes become 1KB;
180 */
181 static function convert_size($size) {
182 if(is_numeric($size)) {
183 $size = (int)$size;
184
185 if($size < 1024) {
186 return $size . 'B';
187 } elseif($size < 1048576) {
188 return round($size / 1024, 2) . 'KB';
189 } elseif($size < 1099511627776) {
190 return round($size / 1048576, 2) . 'MB';
191 } elseif($size < 1125899906842624) {
192 return round($size / 1099511627776, 2) . 'GB';
193 } else {
194 return round($size / 1125899906842624, 2) . 'TB';
195 }
196 }
197
198 return $size;
199 }
200
201 /**
202 * Appends the file contents on the disc
203 *
204 * @author Art <a.molcanovas@gmail.com>
205 * @throws FE When fopen fails
206 * @return boolean
207 */
208 function append() {
209 \Log::debug('Appended the file');
210
211 return $this->doWrite(self::M_WRITE_END);
212 }
213
214 /**
215 * Performs a write operation
216 *
217 * @author Art <a.molcanovas@gmail.com>
218 *
219 * @param string $mode The write mode - see class constants
220 *
221 * @return File
222 * @throws FE When fopen fails
223 */
224 protected function doWrite($mode) {
225 $this->checkParams();
226 if(!$fp = @fopen($this->filepath, $mode)) {
227 throw new FE('Failed to fopen file ' . $this->filepath, FE::E_FOPEN_FAIL);
228 } else {
229 flock($fp, LOCK_EX);
230 fwrite($fp, $this->content);
231 flock($fp, LOCK_UN);
232 fclose($fp);
233 \Log::debug('Wrote ' . $this->filepath . ' contents');
234
235 return $this;
236 }
237 }
238
239 /**
240 * Checks if the dir and name are set
241 *
242 * @author Art <a.molcanovas@gmail.com>
243 * @return File
244 * @throws FE When the file path is not set
245 */
246 protected function checkParams() {
247 if(!$this->dir || !$this->name) {
248 throw new FE('File path not set', FE::E_PATH_NOT_SET);
249 }
250
251 return $this;
252 }
253
254 /**
255 * Gets the file extension based on the currently set filename
256 *
257 * @author Art <a.molcanovas@gmail.com>
258 *
259 * @param int $depth The depth to search for, e.g. if the file name is
260 * foo.tar.gz, depth=1 would return "gz" while depth=2 would return .tar.gz
261 * @param boolean $only_that_member Only effective if $depth > 1. If FALSE
262 * and the extension is tar.gz, will return "tar.gz", if TRUE, will return "tar".
263 *
264 * @return string
265 * @uses self::get_extension()
266 */
267 function getExtension($depth = 1, $only_that_member = false) {
268 return $this->name ? self::get_extension($this->name, $depth, $only_that_member) : null;
269 }
270
271 /**
272 * Gets the file extension based on name
273 *
274 * @author Art <a.molcanovas@gmail.com>
275 *
276 * @param string $filename The file name
277 * @param int $depth The depth to search for, e.g. if the file name is
278 * foo.tar.gz, depth=1 would return "gz" while depth=2 would return .tar.gz
279 * @param boolean $only_that_member Only effective if $depth > 1. If FALSE
280 * and the extension is tar.gz, will return "tar.gz", if TRUE, will return "tar".
281 *
282 * @return string
283 */
284 static function get_extension($filename, $depth = 1, $only_that_member = false) {
285 $exploded = explode('.', strtolower($filename));
286 if(!is_numeric($depth) || $depth < 1) {
287 $depth = 1;
288 }
289
290 return $only_that_member && $depth > 1 ? get($exploded[count($exploded) - $depth]) :
291 implode('.', array_slice($exploded, $depth * -1));
292 }
293
294 /**
295 * Alias for self::unlink()
296 *
297 * @author Art <a.molcanovas@gmail.com>
298 * @uses self::unlink()
299 * @return boolean
300 */
301 function delete() {
302 return $this->unlink();
303 }
304
305 /**
306 * Deletes the file
307 *
308 * @author Art <a.molcanovas@gmail.com>
309 * @throws FE When the file path is not set
310 * @return boolean
311 */
312 function unlink() {
313 $this->checkParams();
314 if(unlink($this->filepath)) {
315 \Log::debug('Deleted ' . $this->filepath);
316
317 return true;
318 } else {
319 \Log::error('Failed to delete ' . $this->filepath);
320
321 return false;
322 }
323 }
324
325 /**
326 * Gzip-encodes the fetched content
327 *
328 * @author Art <a.molcanovas@gmail.com>
329 *
330 * @param int $level Compression strength (0-9)
331 *
332 * @throws FE When the file doesn't exist
333 * @return boolean
334 */
335 function gzipContent($level = 9) {
336 if(self::$gz) {
337 if(!$this->content) {
338 if($this->filepath && $this->fileExists()) {
339 \Log::debug('File contents not present for gzip: reading them');
340 $this->read();
341 } else {
342 \Log::error('Failed to gzip file contents: file not found');
343 }
344 }
345
346 if($this->content) {
347 $this->content = gzencode($this->content, $level);
348 \Log::debug('Gzipped file contents');
349
350 return true;
351 } else {
352 \Log::error('Failed to gzip file contents: content not present');
353 }
354 } else {
355 \Log::error('Failed to gzip file contents: extension not loaded');
356 }
357
358 return false;
359 }
360
361 /**
362 * Checks whether the file exists at the set path
363 *
364 * @author Art <a.molcanovas@gmail.com>
365 * @throws FE When the file path is not set
366 * @return boolean
367 */
368 function fileExists() {
369 $this->checkParams();
370
371 return file_exists($this->filepath);
372 }
373
374 /**
375 * Reads the file contents into $this->content
376 *
377 * @author Art <a.molcanovas@gmail.com>
378 * @return boolean
379 * @throws FE When the file doesn't exist
380 */
381 function read() {
382 $this->checkParams();
383 if(file_exists($this->filepath)) {
384 $this->content = file_get_contents($this->filepath, true);
385 \Log::debug('Read ' . $this->filepath . ' contents');
386
387 return true;
388 } else {
389 throw new FE($this->filepath . ' doesn\'t exist', FE::E_FILE_NOT_EXISTS);
390 }
391 }
392
393 /**
394 * Gzip-decodes the fetched content
395 *
396 * @author Art <a.molcanovas@gmail.com>
397 * @throws FE When the file doesn't exist
398 * @return boolean
399 */
400 function ungzipContent() {
401 if(self::$gz) {
402 if(!$this->content) {
403 if($this->filepath && $this->fileExists()) {
404 \Log::debug('File contents not present for ungzip: reading them');
405 $this->read();
406 } else {
407 \Log::error('Failed to ungzip file contents: file not found');
408 }
409 }
410
411 if($this->content) {
412 $this->content = gzdecode($this->content);
413 \Log::debug('Ungzipped file contents');
414
415 return true;
416 } else {
417 \Log::error('Failed to ungzip file contents: content not present');
418 }
419 } else {
420 \Log::error('Failed to ungzip file contents: extension not loaded');
421 }
422
423 return false;
424 }
425
426 /**
427 * Overwrites the file contents on the disc
428 *
429 * @author Art <a.molcanovas@gmail.com>
430 * @throws FE When the file path is not set
431 * @return File
432 */
433 function write() {
434 \Log::debug('Overwriting file contents');
435
436 return $this->doWrite(self::M_WRITE_TRUNCATE_BEGIN);
437 }
438
439 /**
440 * Returns a string representation of the object data
441 *
442 * @author Art <a.molcanovas@gmail.com>
443 * @return string
444 */
445 function __toString() {
446 return \lite_debug($this);
447 }
448
449 /**
450 * Returns the file's path in the system
451 *
452 * @author Art <a.molcanovas@gmail.com>
453 * @return string
454 */
455 function getFilePath() {
456 return $this->filepath;
457 }
458
459 /**
460 * If no argument is passed, gets the file name, otherwise sets it
461 *
462 * @author Art <a.molcanovas@gmail.com>
463 *
464 * @param string $name The name
465 *
466 * @return File|string
467 * @throws FE When the name is invalid
468 */
469 function name($name = '') {
470 if($name === '') {
471 return $this->name;
472 } elseif(is_scalar($name)) {
473 $this->replace($name);
474 $this->name = trim($name, DIRECTORY_SEPARATOR);
475 $this->updatePath();
476 \Log::debug('File name set to ' . $this->name);
477
478 } else {
479 throw new FE('File name invalid', FE::E_NAME_INVALID);
480 }
481
482 return $this;
483 }
484
485 /**
486 * Updates the file path when the directory or file name are changed
487 *
488 * @author Art <a.molcanovas@gmail.com>
489 * @return File
490 */
491 protected function updatePath() {
492 $this->filepath = $this->dir . DIRECTORY_SEPARATOR . $this->name;
493
494 return $this;
495 }
496
497 /**
498 * If no argument is passed, gets the directory name, otherwise sets it
499 *
500 * @author Art <a.molcanovas@gmail.com>
501 *
502 * @param string $dir The directory
503 *
504 * @return File|string
505 * @throws FE When the name is invalid
506 */
507 function dir($dir = '') {
508 if($dir === '') {
509 return $this->dir;
510 } elseif(is_scalar($dir)) {
511 $this->replace($dir);
512 $this->dir = rtrim($dir, DIRECTORY_SEPARATOR);
513 $this->updatePath();
514 \Log::debug('Directory name set to ' . $dir);
515
516 } else {
517 throw new FE('Directory name invalid', FE::E_NAME_INVALID);
518 }
519
520 return $this;
521 }
522
523 /**
524 * Scans the directory for files
525 *
526 * @author Art <a.molcanovas@gmail.com>
527 * @return array
528 */
529 function scandir() {
530 return $this->dir ? scandir($this->dir) : [];
531 }
532
533 /**
534 * If no argument is passed, gets the currently set content, otherwise sets it
535 *
536 * @author Art <a.molcanovas@gmail.com>
537 *
538 * @param string $content Content to set
539 *
540 * @throws FE When content is not scalar
541 * @return File|string
542 */
543 function content($content = '~none~') {
544 if($content === '~none~') {
545 return $this->content;
546 } elseif(is_scalar($content)) {
547 \Log::debug('Overwrote file contents');
548 $this->content = $content;
549
550 } else {
551 throw new FE('Content is not scalar!', FE::E_CONTENT_INVALID);
552 }
553
554 return $this;
555 }
556
557 /**
558 * Clears the file content
559 *
560 * @author Art <a.molcanovas@gmail.com>
561 * @return File
562 */
563 function clearContent() {
564 \Log::debug('Cleared file contents');
565 $this->content = null;
566
567 return $this;
568 }
569
570 /**
571 * Appends the file content
572 *
573 * @author Art <a.molcanovas@gmail.com>
574 *
575 * @param string $c The content
576 *
577 * @return File
578 */
579 function addContent($c) {
580 \Log::debug('Appended file contents');
581 $this->content .= $c;
582
583 return $this;
584 }
585
586 }