1 <?php
2
3 namespace Alo;
4
5 use Alo\Exception\FileException 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 {
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 * The file content
114 *
115 * @var string
116 */
117 protected $content;
118
119 /**
120 * The file name
121 *
122 * @var string
123 */
124 protected $name;
125
126 /**
127 * The file directory
128 *
129 * @var string
130 */
131 protected $dir;
132
133 /**
134 * Replacements for placeholders
135 *
136 * @var array
137 */
138 protected $replace;
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 * Whether GZIP is installed
151 *
152 * @var boolean
153 */
154 protected static $gz;
155
156 /**
157 * Instantiates the class
158 *
159 * @author Art <a.molcanovas@gmail.com>
160 */
161 function __construct() {
162 $time = time();
163 $this->replace = [
164 'search' => [
165 '{timestamp}',
166 '{datetime}',
167 '{date}',
168 '{time}',
169 '{year}',
170 '{month}',
171 '{day}',
172 '{weekday}'
173 ],
174 'replace' => [
175 $time,
176 date('Y-m-d H.i.s', $time),
177 date('Y-m-d', $time),
178 date('H.i.s', $time),
179 date('Y', $time),
180 date('m', $time),
181 date('d', $time),
182 date('l', $time)
183 ]
184 ];
185
186 $this->dir = DIR_TMP;
187 self::$gz = function_exists('gzencode');
188 }
189
190 /**
191 * Converts a filesize for display
192 *
193 * @author Art <a.molcanovas@gmail.com>
194 * @param int $size The file size in bytes
195 * @return string The file size in its largest form, e.g. 1024 bytes become 1KB;
196 */
197 static function convert_size($size) {
198 if (is_numeric($size)) {
199 $size = (int)$size;
200
201 if ($size < 1024) {
202 return $size . 'B';
203 } elseif ($size < 1048576) {
204 return round($size / 1024, 2) . 'KB';
205 } elseif ($size < 1099511627776) {
206 return round($size / 1048576, 2) . 'MB';
207 } elseif ($size < 1125899906842624) {
208 return round($size / 1099511627776, 2) . 'GB';
209 } else {
210 return round($size / 1125899906842624, 2) . 'TB';
211 }
212 }
213
214 return $size;
215 }
216
217 /**
218 * Appends the file contents on the disc
219 *
220 * @author Art <a.molcanovas@gmail.com>
221 * @return boolean
222 */
223 function append() {
224 \Log::debug('Appended the file');
225
226 return $this->doWrite(self::M_WRITE_END);
227 }
228
229 /**
230 * Perform placeholder replacement operations
231 *
232 * @author Art <a.molcanovas@gmail.com>
233 * @param string $subject The string to perform operations in
234 */
235 protected function replace(&$subject) {
236 $subject = str_ireplace($this->replace['search'], $this->replace['replace'], $subject);
237 }
238
239 /**
240 * Gets the file extension based on name
241 *
242 * @author Art <a.molcanovas@gmail.com>
243 * @param string $filename The file name
244 * @param int $depth The depth to search for, e.g. if the file name is
245 * foo.tar.gz, depth=1 would return "gz" while depth=2 would return .tar.gz
246 * @param boolean $only_that_member Only effective if $depth > 1. If FALSE
247 * and the extension is tar.gz, will return "tar.gz", if TRUE, will return "tar".
248 * @return string
249 */
250 static function get_extension($filename, $depth = 1, $only_that_member = false) {
251 $exploded = explode('.', strtolower($filename));
252 if (!is_numeric($depth) || $depth < 1) {
253 $depth = 1;
254 }
255
256 return $only_that_member && $depth > 1 ? get($exploded[count($exploded) - $depth]) : implode('.', array_slice($exploded, $depth * -1));
257 }
258
259 /**
260 * Gets the file extension based on the currently set filename
261 *
262 * @author Art <a.molcanovas@gmail.com>
263 * @param int $depth The depth to search for, e.g. if the file name is
264 * foo.tar.gz, depth=1 would return "gz" while depth=2 would return .tar.gz
265 * @param boolean $only_that_member Only effective if $depth > 1. If FALSE
266 * and the extension is tar.gz, will return "tar.gz", if TRUE, will return "tar".
267 * @return string
268 * @uses self::get_extension()
269 */
270 function getExtension($depth = 1, $only_that_member = false) {
271 return $this->name ? self::get_extension($this->name, $depth, $only_that_member) : null;
272 }
273
274 /**
275 * Alias for self::unlink()
276 *
277 * @author Art <a.molcanovas@gmail.com>
278 * @uses self::unlink()
279 * @return boolean
280 */
281 function delete() {
282 return $this->unlink();
283 }
284
285 /**
286 * Deletes the file
287 *
288 * @author Art <a.molcanovas@gmail.com>
289 * @return boolean
290 */
291 function unlink() {
292 $this->checkParams();
293 if (unlink($this->filepath)) {
294 \Log::debug('Deleted ' . $this->filepath);
295
296 return true;
297 } else {
298 \Log::error('Failed to delete ' . $this->filepath);
299
300 return false;
301 }
302 }
303
304 /**
305 * Checks whether the file exists at the set path
306 *
307 * @author Art <a.molcanovas@gmail.com>
308 * @return boolean
309 */
310 function fileExists() {
311 $this->checkParams();
312
313 return file_exists($this->filepath);
314 }
315
316 /**
317 * Gzip-encodes the fetched content
318 *
319 * @author Art <a.molcanovas@gmail.com>
320 * @param int $level Compression strength (0-9)
321 * @return boolean
322 */
323 function gzipContent($level = 9) {
324 if (self::$gz) {
325 if (!$this->content) {
326 if ($this->filepath && $this->fileExists()) {
327 \Log::debug('File contents not present for gzip: reading them');
328 $this->read();
329 } else {
330 \Log::error('Failed to gzip file contents: file not found');
331 }
332 }
333
334 if ($this->content) {
335 $this->content = gzencode($this->content, $level);
336 \Log::debug('Gzipped file contents');
337
338 return true;
339 } else {
340 \Log::error('Failed to gzip file contents: content not present');
341 }
342 } else {
343 \Log::error('Failed to gzip file contents: extension not loaded');
344 }
345
346 return false;
347 }
348
349 /**
350 * Gzip-decodes the fetched content
351 *
352 * @author Art <a.molcanovas@gmail.com>
353 * @return boolean
354 */
355 function ungzipContent() {
356 if (self::$gz) {
357 if (!$this->content) {
358 if ($this->filepath && $this->fileExists()) {
359 \Log::debug('File contents not present for ungzip: reading them');
360 $this->read();
361 } else {
362 \Log::error('Failed to ungzip file contents: file not found');
363 }
364 }
365
366 if ($this->content) {
367 $this->content = gzdecode($this->content);
368 \Log::debug('Ungzipped file contents');
369
370 return true;
371 } else {
372 \Log::error('Failed to ungzip file contents: content not present');
373 }
374 } else {
375 \Log::error('Failed to ungzip file contents: extension not loaded');
376 }
377
378 return false;
379 }
380
381 /**
382 * Overwrites the file contents on the disc
383 *
384 * @author Art <a.molcanovas@gmail.com>
385 * @return File
386 */
387 function write() {
388 \Log::debug('Overwriting file contents');
389
390 return $this->doWrite(self::M_WRITE_TRUNCATE_BEGIN);
391 }
392
393 /**
394 * Performs a write operation
395 *
396 * @author Art <a.molcanovas@gmail.com>
397 * @param string $mode The write mode - see class constants
398 * @return File
399 * @throws FE When fopen fails
400 */
401 protected function doWrite($mode) {
402 $this->checkParams();
403 if (!$fp = @fopen($this->filepath, $mode)) {
404 throw new FE('Failed to fopen file ' . $this->filepath, FE::E_FOPEN_FAIL);
405 } else {
406 flock($fp, LOCK_EX);
407 fwrite($fp, $this->content);
408 flock($fp, LOCK_UN);
409 fclose($fp);
410 \Log::debug('Wrote ' . $this->filepath . ' contents');
411
412 return $this;
413 }
414 }
415
416 /**
417 * Checks if the dir and name are set
418 *
419 * @author Art <a.molcanovas@gmail.com>
420 * @return File
421 * @throws FE When the file path is not set
422 */
423 protected function checkParams() {
424 if (!$this->dir || !$this->name) {
425 throw new FE('File path not set', FE::E_PATH_NOT_SET);
426 }
427
428 return $this;
429 }
430
431 /**
432 * Reads the file contents into $this->content
433 *
434 * @author Art <a.molcanovas@gmail.com>
435 * @return boolean
436 * @throws FE When the file doesn't exist
437 */
438 function read() {
439 $this->checkParams();
440 if (file_exists($this->filepath)) {
441 $this->content = file_get_contents($this->filepath, true);
442 \Log::debug('Read ' . $this->filepath . ' contents');
443
444 return true;
445 } else {
446 throw new FE($this->filepath . ' doesn\'t exist', FE::E_FILE_NOT_EXISTS);
447 }
448 }
449
450 /**
451 * Returns a string representation of the object data
452 *
453 * @author Art <a.molcanovas@gmail.com>
454 * @return string
455 */
456 function __toString() {
457 return \lite_debug($this);
458 }
459
460 /**
461 * Updates the file path when the directory or file name are changed
462 *
463 * @author Art <a.molcanovas@gmail.com>
464 * @return File
465 */
466 protected function updatePath() {
467 $this->filepath = $this->dir . DIRECTORY_SEPARATOR . $this->name;
468
469 return $this;
470 }
471
472 /**
473 * Returns the file's path in the system
474 *
475 * @author Art <a.molcanovas@gmail.com>
476 * @return string
477 */
478 function getFilePath() {
479 return $this->filepath;
480 }
481
482 /**
483 * If no argument is passed, gets the file name, otherwise sets it
484 *
485 * @author Art <a.molcanovas@gmail.com>
486 * @param string $name The name
487 * @return File|string
488 * @throws FE When the name is invalid
489 */
490 function name($name = '') {
491 if ($name === '') {
492 return $this->name;
493 } elseif (is_scalar($name)) {
494 $this->replace($name);
495 $this->name = trim($name, DIRECTORY_SEPARATOR);
496 $this->updatePath();
497 \Log::debug('File name set to ' . $this->name);
498
499 } else {
500 throw new FE('File name invalid', FE::E_NAME_INVALID);
501 }
502
503 return $this;
504 }
505
506 /**
507 * If no argument is passed, gets the directory name, otherwise sets it
508 *
509 * @author Art <a.molcanovas@gmail.com>
510 * @param string $dir The directory
511 * @return File|string
512 * @throws FE When the name is invalid
513 */
514 function dir($dir = '') {
515 if ($dir === '') {
516 return $this->dir;
517 } elseif (is_scalar($dir)) {
518 $this->replace($dir);
519 $this->dir = rtrim($dir, DIRECTORY_SEPARATOR);
520 $this->updatePath();
521 \Log::debug('Directory name set to ' . $dir);
522
523 } else {
524 throw new FE('Directory name invalid', FE::E_NAME_INVALID);
525 }
526
527 return $this;
528 }
529
530 /**
531 * Scans the directory for files
532 *
533 * @author Art <a.molcanovas@gmail.com>
534 * @return array
535 */
536 function scandir() {
537 return $this->dir ? scandir($this->dir) : [];
538 }
539
540 /**
541 * If no argument is passed, gets the currently set content, otherwise sets it
542 *
543 * @author Art <a.molcanovas@gmail.com>
544 * @param string $content Content to set
545 * @throws FE When content is not scalar
546 * @return File|string
547 */
548 function content($content = '~none~') {
549 if ($content === '~none~') {
550 return $this->content;
551 } elseif (is_scalar($content)) {
552 \Log::debug('Overwrote file contents');
553 $this->content = $content;
554
555 } else {
556 throw new FE('Content is not scalar!', FE::E_CONTENT_INVALID);
557 }
558
559 return $this;
560 }
561
562 /**
563 * Clears the file content
564 *
565 * @author Art <a.molcanovas@gmail.com>
566 * @return File
567 */
568 function clearContent() {
569 \Log::debug('Cleared file contents');
570 $this->content = null;
571
572 return $this;
573 }
574
575 /**
576 * Appends the file content
577 *
578 * @author Art <a.molcanovas@gmail.com>
579 * @param string $c The content
580 * @return File
581 */
582 function addContent($c) {
583 \Log::debug('Appended file contents');
584 $this->content .= $c;
585
586 return $this;
587 }
588
589 }