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 } else {
10
11 /**
12 * Object-oriented file handler
13 *
14 * @author Arturas Molcanovas <a.molcanovas@gmail.com>
15 */
16 class File extends AbstractFileSystem {
17
18 /**
19 * Open for reading only; place the file pointer at the beginning of the file.
20 *
21 * @var string
22 */
23 const M_READ_EXISTING_BEGIN = 'r';
24
25 /**
26 * Open for reading and writing; place the file pointer at the beginning of the file.
27 *
28 * @var string
29 */
30 const M_RW_EXISTING_BEGIN = 'r+';
31
32 /**
33 * Open for writing only; place the file pointer at the beginning of the file
34 * and truncate the file to zero length. If the file does not exist, attempt
35 * to create it.
36 *
37 * @var string
38 */
39 const M_WRITE_TRUNCATE_BEGIN = 'w';
40
41 /**
42 * Open for reading and writing; place the file pointer at the beginning of
43 * the file and truncate the file to zero length. If the file does not exist,
44 * attempt to create it.
45 *
46 * @var string
47 */
48 const M_RW_TRUNCATE_BEGIN = 'w+';
49
50 /**
51 * Open for writing only; place the file pointer at the end of the file. If
52 * the file does not exist, attempt to create it.
53 *
54 * @var string
55 */
56 const M_WRITE_END = 'a';
57
58 /**
59 * Open for reading and writing; place the file pointer at the end of the file.
60 * If the file does not exist, attempt to create it.
61 *
62 * @var string
63 */
64 const M_RW_END = 'a+';
65
66 /**
67 * Create and open for writing only; place the file pointer at the beginning
68 * of the file. If the file already exists, the fopen() call will fail by
69 * returning FALSE and generating an error of level E_WARNING. If the file
70 * does not exist, attempt to create it. This is equivalent to specifying
71 * O_EXCL|O_CREAT flags for the underlying open(2) system call.
72 *
73 * @var string
74 */
75 const M_WRITE_NONEXIST_BEGIN = 'x';
76
77 /**
78 * Create and open for reading and writing; otherwise it has the same behavior as
79 * M_WRITE_NONEXIST_BEGIN
80 *
81 * @var string
82 * @see self::M_WRITE_NONEXIST_BEGIN
83 */
84 const M_RW_NONEXIST_BEGIN = 'x+';
85
86 /**
87 * Open the file for writing only. If the file does not exist, it is created.
88 * If it exists, it is neither truncated (as opposed to M_WRITE_TRUNCATE_BEGIN), nor the call to
89 * this function fails (as is the case with M_WRITE_NONEXIST_BEGIN). The file pointer is positioned
90 * on the beginning of the file. This may be useful if it's desired to get an
91 * advisory lock (see flock()) before attempting to modify the file, as using
92 * M_WRITE_NONEXIST_BEGIN could truncate the file before the lock was
93 * obtained (if truncation is desired, ftruncate() can be used after the lock
94 * is requested).
95 *
96 * @var string
97 * @see self::M_WRITE_NONEXIST_BEGIN
98 * @see self::M_WRITE_TRUNCATE_BEGIN
99 */
100 const M_WRITE_BEGIN = 'c';
101
102 /**
103 * Open the file for reading and writing; otherwise it has the same behavior as
104 * M_WRITE_BEGIN.
105 *
106 * @var string
107 * @see self::M_WRITE_BEGIN;
108 */
109 const M_RW_BEGIN = 'c+';
110
111 /**
112 * Whether GZIP is installed
113 *
114 * @var boolean
115 */
116 protected static $gz;
117
118 /**
119 * The file content
120 *
121 * @var string
122 */
123 protected $content;
124
125 /**
126 * The file name
127 *
128 * @var string
129 */
130 protected $name;
131
132 /**
133 * The file directory
134 *
135 * @var string
136 */
137 protected $dir;
138
139 /**
140 * The full file path. Updates with every setName & setDir
141 *
142 * @var string
143 * @see self::setName()
144 * @see self::setDir()
145 */
146 protected $filepath;
147
148 /**
149 * Instantiates the class
150 *
151 * @author Art <a.molcanovas@gmail.com>
152 */
153 function __construct() {
154 parent::__construct();
155
156 $this->dir = DIR_TMP;
157 self::$gz = function_exists('gzencode');
158 }
159
160 /**
161 * Instantiates the class
162 *
163 * @author Art <a.molcanovas@gmail.com>
164 *
165 * @return File
166 */
167 static function file() {
168 return new File();
169 }
170
171 /**
172 * Converts a filesize for display
173 *
174 * @author Art <a.molcanovas@gmail.com>
175 *
176 * @param int $size The file size in bytes
177 *
178 * @return string The file size in its largest form, e.g. 1024 bytes become 1KB;
179 */
180 static function convertSize($size) {
181 if (is_numeric($size)) {
182 $size = (int)$size;
183
184 if ($size < 1024) {
185 return $size . 'B';
186 } elseif ($size < 1048576) {
187 return round($size / 1024, 2) . 'KB';
188 } elseif ($size < 1099511627776) {
189 return round($size / 1048576, 2) . 'MB';
190 } elseif ($size < 1125899906842624) {
191 return round($size / 1099511627776, 2) . 'GB';
192 } else {
193 return round($size / 1125899906842624, 2) . 'TB';
194 }
195 }
196
197 return $size;
198 }
199
200 /**
201 * Appends the file contents on the disc
202 *
203 * @author Art <a.molcanovas@gmail.com>
204 * @throws FE When fopen fails
205 * @return boolean
206 */
207 function append() {
208 \Log::debug('Appended the file');
209
210 return $this->doWrite(self::M_WRITE_END);
211 }
212
213 /**
214 * Performs a write operation
215 *
216 * @author Art <a.molcanovas@gmail.com>
217 *
218 * @param string $mode The write mode - see class constants
219 *
220 * @return File
221 * @throws FE When fopen fails
222 */
223 protected function doWrite($mode) {
224 $this->checkParams();
225
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 $onlyThatMember 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, $onlyThatMember = false) {
268 return $this->name ? self::getExtensionStatically($this->name, $depth, $onlyThatMember) : 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 $onlyThatMember 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 getExtensionStatically($filename, $depth = 1, $onlyThatMember = false) {
285 $exploded = explode('.', strtolower($filename));
286 if (!is_numeric($depth) || $depth < 1) {
287 $depth = 1;
288 }
289
290 return $onlyThatMember && $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 \debugLite($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 }
587 }
588