root/fFilesystem.php

Revision 315, 18.1 kB (checked in by wbond, 2 years ago)

More API documentation updates

LineHide Line Numbers
1 <?php
2 /**
3  * Handles filesystem-level tasks including filesystem transactions and the reference map to keep all fFile and fDirectory objects in sync
4  *
5  * @copyright  Copyright (c) 2008 William Bond
6  * @author     William Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fFilesystem
11  *
12  * @version    1.0.0b
13  * @changes    1.0.0b  The initial implementation [wb, 2008-03-24]
14  */
15 class fFilesystem
16 {
17     // The following constants allow for nice looking callbacks to static methods
18     const addWebPathTranslation         = 'fFilesystem::addWebPathTranslation';
19     const begin                         = 'fFilesystem::begin';
20     const commit                        = 'fFilesystem::commit';
21     const convertToBytes                = 'fFilesystem::convertToBytes';
22     const formatFilesize                = 'fFilesystem::formatFilesize';
23     const getPathInfo                   = 'fFilesystem::getPathInfo';
24     const hookExceptionMap              = 'fFilesystem::hookExceptionMap';
25     const hookFilenameMap               = 'fFilesystem::hookFilenameMap';
26     const isInsideTransaction           = 'fFilesystem::isInsideTransaction';
27     const makeUniqueName                = 'fFilesystem::makeUniqueName';
28     const recordCreate                  = 'fFilesystem::recordCreate';
29     const recordDelete                  = 'fFilesystem::recordDelete';
30     const recordDuplicate               = 'fFilesystem::recordDuplicate';
31     const recordRename                  = 'fFilesystem::recordRename';
32     const recordWrite                   = 'fFilesystem::recordWrite';
33     const reset                         = 'fFilesystem::reset';
34     const rollback                      = 'fFilesystem::rollback';
35     const translateToWebPath            = 'fFilesystem::translateToWebPath';
36     const updateExceptionMap            = 'fFilesystem::updateExceptionMap';
37     const updateFilenameMap             = 'fFilesystem::updateFilenameMap';
38     const updateFilenameMapForDirectory = 'fFilesystem::updateFilenameMapForDirectory';
39    
40    
41     /**
42     * Stores the operations to perform when a commit occurs
43     *
44     * @var array
45     */
46     static private $commit_operations = NULL;
47    
48     /**
49     * Maps exceptions to all instances of a file or directory, providing consistency
50     *
51     * @var array
52     */
53     static private $exception_map = array();
54    
55     /**
56     * Stores file and directory names by reference, allowing all object instances to be updated at once
57     *
58     * @var array
59     */
60     static private $filename_map = array();
61    
62     /**
63     * Stores the operations to perform if a rollback occurs
64     *
65     * @var array
66     */
67     static private $rollback_operations = NULL;
68    
69     /**
70     * Stores a list of search => replace strings for web path translations
71     *
72     * @var array
73     */
74     static private $web_path_translations = array();
75    
76    
77     /**
78     * Adds a directory to the web path translation list
79     *
80     * The web path conversion list is a list of directory paths that will be
81     * converted (from the beginning of filesystem paths) when preparing a path
82     * for output into HTML.
83     *
84     * By default the `$_SERVER['DOCUMENT_ROOT']` will be converted to a blank
85     * string, in essence stripping it from filesystem paths.
86     *
87     * @param  string $search_path   The path to look for
88     * @param  string $replace_path  The path to replace with
89     * @return void
90     */
91     static public function addWebPathTranslation($search_path, $replace_path)
92     {
93         // Ensure we have the correct kind of slash for the OS being used
94         $search_path  = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $search_path);
95         $replace_path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $replace_path);
96         self::$web_path_translations[$search_path] = $replace_path;
97     }
98    
99    
100     /**
101     * Starts a filesystem pseudo-transaction, should only be called when no transaction is in progress.
102     *
103     * Flourish filesystem transactions are NOT full ACID-compliant
104     * transactions, but rather more of an filesystem undo buffer which can
105     * return the filesystem to the state when ::begin() was called. If your PHP
106     * script dies in the middle of an operation this functionality will do
107     * nothing for you and all operations will be retained, except for deletes
108     * which only occur once the transaction is committed.
109     *
110     * @return void
111     */
112     static public function begin()
113     {
114         if (self::$commit_operations !== NULL) {
115             fCore::toss(
116                 'fProgrammerException',
117                 fGrammar::compose(
118                     'There is already a filesystem transaction in progress'
119                 )
120             );
121         }
122         self::$commit_operations   = array();
123         self::$rollback_operations = array();
124     }
125    
126    
127     /**
128     * Commits a filesystem transaction, should only be called when a transaction is in progress
129     *
130     * @return void
131     */
132     static public function commit()
133     {
134         if (!self::isInsideTransaction()) {
135             fCore::toss(
136                 'fProgrammerException',
137                 fGrammar::compose(
138                     'There is no filesystem transaction in progress to commit'
139                 )
140             );
141         }
142        
143         $commit_operations = self::$commit_operations;
144        
145         self::$commit_operations   = NULL;
146         self::$rollback_operations = NULL;
147        
148         $commit_operations = array_reverse($commit_operations);
149        
150         foreach ($commit_operations as $operation) {
151             // Commit operations only include deletes, however it could be a filename or object
152             if (isset($operation['filename'])) {
153                 @unlink($operation['filename']);
154             } else {
155                 $operation['object']->delete();
156             }
157         }
158     }
159    
160    
161     /**
162     * Takes a file size including a unit of measure (i.e. kb, GB, M) and converts it to bytes
163     *
164     * @param  string $size  The size to convert to bytes
165     * @return integer  The number of bytes represented by the size
166     */
167     static public function convertToBytes($size)
168     {
169         if (!preg_match('#^(\d+)\s*(k|m|g|t)?(ilo|ega|era|iga)?( )?b?(yte(s)?)?$#', strtolower(trim($size)), $matches)) {
170             fCore::toss(
171                 'fProgrammerException',
172                 fGrammar::compose(
173                     'The size specified, %s, does not appears to be a valid size',
174                     fCore::dump($size)
175                 )
176             );
177         }
178        
179         if ($matches[2] == '') {
180             $matches[2] = 'b';
181         }
182        
183         $size_map = array('b' => 1,
184                           'k' => 1024,
185                           'm' => 1048576,
186                           'g' => 1073741824,
187                           't' => 1099511627776);
188         return $matches[1] * $size_map[$matches[2]];
189     }
190    
191    
192     /**
193     * Takes the size of a file in bytes and returns a friendly size in b/kb/mb/gb/tb
194     *
195     * @param  integer $bytes           The size of the file in bytes
196     * @param  integer $decimal_places  The number of decimal places to display
197     * @return string
198     */
199     static public function formatFilesize($bytes, $decimal_places=1)
200     {
201         if ($bytes < 0) {
202             $bytes = 0;
203         }
204         $suffixes  = array('b', 'kb', 'mb', 'gb', 'tb');
205         $sizes     = array(1, 1024, 1048576, 1073741824, 1099511627776);
206         $suffix    = floor(log($bytes)/6.9314718);
207         return number_format($bytes/$sizes[$suffix], $decimal_places) . $suffixes[$suffix];
208     }
209    
210    
211     /**
212     * Returns info about a path including dirname, basename, extension and filename
213     *
214     * @param  string $file_path  The file to rename
215     * @param  string $element    The piece of information to return: `'dirname'`, `'basename'`, `'extension'`, or `'filename'`
216     * @return array  The file's dirname, basename, extension and filename
217     */
218     static public function getPathInfo($file, $element=NULL)
219     {
220         $valid_elements = array('dirname', 'basename', 'extension', 'filename');
221         if ($element !== NULL && !in_array($element, $valid_elements)) {
222             fCore::toss(
223                 'fProgrammerException',
224                 fGrammar::compose(
225                     'The element specified, %1$s, is invalid. Must be one of: %2$s.',
226                     fCore::dump($element),
227                     join(', ', $valid_elements)
228                 )
229             );
230         }
231        
232         $path_info = pathinfo($file);
233        
234         if (!isset($path_info['extension'])) {
235             $path_info['extension'] = NULL;
236         }
237        
238         if (!isset($path_info['filename'])) {
239             $path_info['filename'] = preg_replace('#\.' . preg_quote($path_info['extension'], '#') . '$#', '', $path_info['basename']);
240         }
241         $path_info['dirname'] .= DIRECTORY_SEPARATOR;
242        
243         if ($element) {
244             return $path_info[$element];
245         }
246        
247         return $path_info;
248     }
249    
250    
251     /**
252     * Hooks a file/directory into the exception map entry for that filename
253     *
254     * Since the value is returned by reference, all objects that represent
255     * this file/directory always see the same exception.
256     *
257     * @internal
258      *
259     * @param  string $file  The name of the file or directory
260     * @return mixed  Will return `NULL` if no match, or the exception object if a match occurs
261     */
262     static public function &hookExceptionMap($file)
263     {
264         if (!isset(self::$exception_map[$file])) {
265             self::$exception_map[$file] = NULL;
266         }
267         return self::$exception_map[$file];
268     }
269    
270    
271     /**
272     * Hooks a file/directory name to the filename map
273     *
274     * Since the value is returned by reference, all objects that represent
275     * this file/directory will always be update on a rename.
276     *
277     * @internal
278      *
279     * @param  string $file  The name of the file or directory
280     * @return mixed  Will return `NULL` if no match, or the exception object if a match occurs
281     */
282     static public function &hookFilenameMap($file)
283     {
284         if (!isset(self::$filename_map[$file])) {
285             self::$filename_map[$file] = $file;
286         }
287         return self::$filename_map[$file];
288     }
289    
290    
291     /**
292     * Indicates if a transaction is in progress
293     *
294     * @return void
295     */
296     static public function isInsideTransaction()
297     {
298         return is_array(self::$commit_operations);
299     }
300    
301    
302     /**
303     * Returns a unique name for a file
304     *
305     * @param  string $file           The filename to check
306     * @param  string $new_extension  The new extension for the filename, should not include `.`
307     * @return string  The unique file name
308     */
309     static public function makeUniqueName($file, $new_extension=NULL)
310     {
311         $info = self::getPathInfo($file);
312        
313         // Change the file extension
314         if ($new_extension !== NULL) {
315             $new_extension = ($new_extension) ? '.' . $new_extension : $new_extension;
316             $file = $info['dirname'] . $info['filename'] . $new_extension;
317             $info = self::getPathInfo($file);
318         }
319        
320         // If there is an extension, be sure to add . before it
321         $extension = (!empty($info['extension'])) ? '.' . $info['extension'] : '';
322        
323         // Remove _copy# from the filename to start
324         $file = preg_replace('#_copy(\d+)' . preg_quote($extension, '#') . '$#', $extension, $file);
325        
326         // Look for a unique name by adding _copy# to the end of the file
327         while (file_exists($file)) {
328             $info = self::getPathInfo($file);
329             if (preg_match('#_copy(\d+)' . preg_quote($extension, '#') . '$#', $file, $match)) {
330                 $file = preg_replace('#_copy(\d+)' . preg_quote($extension, '#') . '$#', '_copy' . ($match[1]+1) . $extension, $file);
331             } else {
332                 $file = $info['dirname'] . $info['filename'] . '_copy1' . $extension;
333             }
334         }
335        
336         return $file;
337     }
338    
339    
340     /**
341     * Saves an object to the identity map
342     *
343     * @internal
344      *
345     * @param  string    $file         A file or directory name, directories should end in `/` or `\`
346     * @param  Exception $exception  The exception for this file/directory
347     * @return void
348     */
349     static public function updateExceptionMap($file, Exception $exception)
350     {
351         self::$exception_map[$file] = $exception;
352     }
353    
354    
355     /**
356     * Updates the filename map, causing all objects representing a file/directory to be updated
357     *
358     * @internal
359      *
360     * @param  string $existing_filename  The existing filename
361     * @param  string $new_filename       The new filename
362     * @return void
363     */
364     static public function updateFilenameMap($existing_filename, $new_filename)
365     {
366         if ($existing_filename == $new_filename) {
367             return;
368         }
369        
370         self::$filename_map[$new_filename]  =& self::$filename_map[$existing_filename];
371         self::$exception_map[$new_filename] =& self::$exception_map[$existing_filename];
372        
373         unset(self::$filename_map[$existing_filename]);
374         unset(self::$exception_map[$existing_filename]);
375        
376         self::$filename_map[$new_filename] = $new_filename;
377     }
378    
379    
380     /**
381     * Updates the filename map recursively, causing all objects representing a directory to be updated
382     *
383     * Also updates all files and directories in the specified directory to the new paths.
384     *
385     * @internal
386      *
387     * @param  string $existing_dirname  The existing directory name
388     * @param  string $new_dirname       The new dirname
389     * @return void
390     */
391     static public function updateFilenameMapForDirectory($existing_dirname, $new_dirname)
392     {
393         if ($existing_dirname == $new_dirname) {
394             return;
395         }
396        
397         // Handle the directory name
398         self::$filename_map[$new_dirname]  =& self::$filename_map[$existing_dirname];
399         self::$exception_map[$new_dirname] =& self::$exception_map[$existing_dirname];
400        
401         unset(self::$filename_map[$existing_dirname]);
402         unset(self::$exception_map[$existing_dirname]);
403        
404         self::$filename_map[$new_dirname] = $new_dirname;
405        
406         // Handle all of the directories and files inside this directory
407         foreach (self::$filename_map as $filename => $ignore) {
408             if (preg_match('#^' . preg_quote($existing_dirname, '#') . '#', $filename)) {
409                 $new_filename = preg_replace('#^' . preg_quote($existing_dirname, '#') . '#', $new_dirname, $filename);
410                
411                 self::$filename_map[$new_filename]  =& self::$filename_map[$filename];
412                 self::$exception_map[$new_filename] =& self::$exception_map[$filename];
413                
414                 unset(self::$filename_map[$filename]);
415                 unset(self::$exception_map[$filename]);
416                
417                 self::$filename_map[$new_filename] = $new_filename;
418                    
419             }
420         }
421     }
422    
423    
424     /**
425     * Keeps a record of created files so they can be deleted up in case of a rollback
426     *
427     * @internal
428      *
429     * @param  object $object  The new file or directory to get rid of on rollback
430     * @return void
431     */
432     static public function recordCreate($object)
433     {
434         self::$rollback_operations[] = array(
435             'action' => 'delete',
436             'object' => $object
437         );
438     }
439    
440    
441     /**
442     * Keeps track of file and directory names to delete when a transaction is committed
443     *
444     * @internal
445      *
446     * @param  fFile|fDirectory $object  The filesystem object to delete
447     * @return void
448     */
449     static public function recordDelete($object)
450     {
451         self::$commit_operations[] = array(
452             'action' => 'delete',
453             'object' => $object
454         );
455     }
456    
457    
458     /**
459     * Keeps a record of duplicated files so they can be cleaned up in case of a rollback
460     *
461     * @internal
462      *
463     * @param  fFile $file  The duplicate file to get rid of on rollback
464     * @return void
465     */
466     static public function recordDuplicate(fFile $file)
467     {
468         self::$rollback_operations[] = array(
469             'action'   => 'delete',
470             'filename' => $file->getPath()
471         );
472     }
473    
474    
475     /**
476     * Keeps a temp file in place of the old filename so the file can be restored during a rollback
477     *
478     * @internal
479      *
480     * @param  string $old_name  The old file or directory name
481     * @param  string $new_name  The new file or directory name
482     * @return void
483     */
484     static public function recordRename($old_name, $new_name)
485     {
486         self::$rollback_operations[] = array(
487             'action'   => 'rename',
488             'old_name' => $old_name,
489             'new_name' => $new_name
490         );
491        
492         // Create the file with no content to prevent overwriting by another process
493         file_put_contents($old_name, '');
494        
495         self::$commit_operations[] = array(
496             'action'   => 'delete',
497             'filename' => $old_name
498         );
499     }
500    
501    
502     /**
503     * Keeps backup copies of files so they can be restored if there is a rollback
504     *
505     * @internal
506      *
507     * @param  fFile $file  The file that is being written to
508     * @return void
509     */
510     static public function recordWrite(fFile $file)
511     {
512         self::$rollback_operations[] = array(
513             'action'   => 'write',
514             'filename' => $file->getPath(),
515             'old_data' => file_get_contents($file->getPath())
516         );
517     }
518    
519    
520     /**
521     * Resets the configuration of the class
522     *
523     * @internal
524      *
525     * @return void
526     */
527     static public function reset()
528     {
529         self::rollback();
530         self::$exception_map         = array();
531         self::$filename_map          = array();
532         self::$web_path_translations = array();   
533     }
534    
535    
536     /**
537     * Rolls back a filesystem transaction, it is safe to rollback when no transaction is in progress
538     *
539     * @return void
540     */
541     static public function rollback()
542     {
543         self::$rollback_operations = array_reverse(self::$rollback_operations);
544        
545         foreach (self::$rollback_operations as $operation) {
546             switch($operation['action']) {
547                
548                 case 'delete':
549                     @unlink($operation['filename']);
550                     break;
551                    
552                 case 'write':
553                     file_put_contents($operation['filename'], $operation['old_data']);
554                     break;
555                    
556                 case 'rename':
557                     @rename($operation['new_name'], $operation['old_name']);
558                     break;
559                    
560             }
561         }
562        
563         // All files to be deleted should have their exceptions erased
564         foreach (self::$commit_operations as $operation) {
565             if (isset($operation['object'])) {
566                 self::updateExceptionMap($operation['object']->getPath(), NULL);
567             }
568         }
569        
570         self::$commit_operations   = NULL;
571         self::$rollback_operations = NULL;
572     }
573    
574    
575     /**
576     * Takes a filesystem path and translates it to a web path using the rules added
577     *
578     * @param  string $path  The path to translate
579     * @return string  The filesystem path translated to a web path
580     */
581     static public function translateToWebPath($path)
582     {
583         $translations = array(realpath($_SERVER['DOCUMENT_ROOT']) => '') + self::$web_path_translations;
584        
585         foreach ($translations as $search => $replace) {
586             $path = preg_replace('#^' . preg_quote($search, '#') . '#', $replace, $path);
587         }
588        
589         return $path;
590     }
591    
592    
593     /**
594     * Forces use as a static class
595     *
596     * @return fFilesystem
597     */
598     private function __construct() { }
599 }
600  
601  
602  
603 /**
604  * Copyright (c) 2008 William Bond <will@flourishlib.com>
605  *
606  * Permission is hereby granted, free of charge, to any person obtaining a copy
607  * of this software and associated documentation files (the "Software"), to deal
608  * in the Software without restriction, including without limitation the rights
609  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
610  * copies of the Software, and to permit persons to whom the Software is
611  * furnished to do so, subject to the following conditions:
612  *
613  * The above copyright notice and this permission notice shall be included in
614  * all copies or substantial portions of the Software.
615  *
616  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
617  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
618  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
619  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
620  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
621  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
622  * THE SOFTWARE.
623  */