Ticket #219: FileSystem.php

File FileSystem.php, 23.5 kB (added by hans, 10 months ago)

updated FileSystem class (trunk)

Line 
1 <?php
2
3 /*
4  *  $Id: FileSystem.php 313 2007-11-17 04:20:58Z hans $
5  *
6  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
7  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
8  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
9  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
10  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
12  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
13  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
14  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
15  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
16  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
17  *
18  * This software consists of voluntary contributions made by many individuals
19  * and is licensed under the LGPL. For more information please see
20  * <http://phing.info>.
21  */
22
23 namespace phing::system::io;
24 use phing::BuildException;
25 use phing::Phing;
26
27 /**
28  * This is an abstract class for platform specific filesystem implementations
29  * you have to implement each method in the platform specific filesystem implementation
30  * classes Your local filesytem implementation must extend this class.
31  * You should also use this class as a template to write your local implementation
32  * Some native PHP filesystem specific methods are abstracted here as well. Anyway
33  * you _must_ always use this methods via a File object (that by nature uses the
34  * *FileSystem drivers to access the real filesystem via this class using natives.
35  *
36  * FIXME:
37  *  - Error handling reduced to min fallthrough runtime excetions
38  *    more precise errorhandling is done by the File class
39  *
40  * @author Charlie Killian <charlie@tizac.com>
41  * @author Hans Lellelid <hans@xmpl.org>
42  * @version $Revision: 1.11 $
43  * @package phing.system.io
44  */
45 abstract class FileSystem {
46
47     /* properties for simple boolean attributes */
48     const BA_EXISTS    = 0x01;
49     const BA_REGULAR   = 0x02;
50     const BA_DIRECTORY = 0x04;
51     const BA_HIDDEN    = 0x08;
52
53     /**
54      * Singleton instance for getFileSystem() method.
55      * @var FileSystem
56      */
57     private static $fs;
58
59     /**
60      * Static method to return the FileSystem singelton representing
61      * this platform's local filesystem driver.
62      * @return FileSystem
63      */
64     public static function getFileSystem() {
65         if (self::$fs === null) {
66             switch(Phing::getProperty('host.fstype')) {
67                 case 'UNIX':
68                     self::$fs = new UnixFileSystem();
69                 break;
70                 case 'WIN32':
71                     self::$fs = new Win32FileSystem();
72                 break;
73                 case 'WINNT':
74                     self::$fs = new WinNTFileSystem();
75                 break;
76                 default:
77                     throw new Exception("Host uses unsupported filesystem, unable to proceed");
78             }
79         }
80         return self::$fs;
81     }
82
83     /* -- Normalization and construction -- */
84
85     /**
86      * Return the local filesystem's name-separator character.
87      */
88     abstract function getSeparator();
89
90     /**
91      * Return the local filesystem's path-separator character.
92      */
93     abstract function getPathSeparator();
94
95     /**
96      * Convert the given pathname string to normal form.  If the string is
97      * already in normal form then it is simply returned.
98      */
99     abstract function normalize($strPath);
100
101     /**
102      * Compute the length of this pathname string's prefix.  The pathname
103      * string must be in normal form.
104      */
105     abstract function prefixLength($pathname);
106
107     /**
108      * Resolve the child pathname string against the parent.
109      * Both strings must be in normal form, and the result
110      * will be a string in normal form.
111      */
112     abstract function resolve($parent, $child);
113
114     /**
115      * Resolve the given abstract pathname into absolute form.  Invoked by the
116      * getAbsolutePath and getCanonicalPath methods in the File class.
117      */
118     abstract function resolveFile(File $f);
119
120     /**
121      * Return the parent pathname string to be used when the parent-directory
122      * argument in one of the two-argument File constructors is the empty
123      * pathname.
124      */
125     abstract function getDefaultParent();
126
127     /**
128      * Post-process the given URI path string if necessary.  This is used on
129      * win32, e.g., to transform "/c:/foo" into "c:/foo".  The path string
130      * still has slash separators; code in the File class will translate them
131      * after this method returns.
132      */
133     abstract function fromURIPath($path);
134
135     /* -- Path operations -- */
136
137     /**
138      * Tell whether or not the given abstract pathname is absolute.
139      */
140     abstract function isAbsolute(File $f);
141
142     /**
143      * canonicalize filename by checking on disk
144      * @return mixed Canonical path or false if the file doesn't exist.
145      */
146     function canonicalize($strPath) {
147         return @realpath($strPath);
148     }
149
150     /* -- Attribute accessors -- */
151
152     /**
153      * Return the simple boolean attributes for the file or directory denoted
154      * by the given abstract pathname, or zero if it does not exist or some
155      * other I/O error occurs.
156      */
157     function getBooleanAttributes($f) {
158         throw new Exception("SYSTEM ERROR method getBooleanAttributes() not implemented by fs driver");
159     }
160
161     /**
162      * Check whether the file or directory denoted by the given abstract
163      * pathname may be accessed by this process.  If the second argument is
164      * false, then a check for read access is made; if the second
165      * argument is true, then a check for write (not read-write)
166      * access is made.  Return false if access is denied or an I/O error
167      * occurs.
168      */
169     function checkAccess(File $f, $write = false) {
170         // we clear stat cache, its expensive to look up from scratch,
171         // but we need to be sure
172         @clearstatcache();
173
174
175         // Shouldn't this be $f->GetAbsolutePath() ?
176         // And why doesn't GetAbsolutePath() work?
177
178         $strPath = (string) $f->getPath();
179
180         // FIXME
181         // if file object does denote a file that yet not existst
182         // path rights are checked
183         if (!@file_exists($strPath) && !is_dir($strPath)) {
184             $strPath = $f->getParent();
185             if ($strPath === null || !is_dir($strPath)) {
186                 $strPath = Phing::getProperty("user.dir");
187             }
188             //$strPath = dirname($strPath);
189         }
190
191         if (!$write) {
192             return (boolean) @is_readable($strPath);
193         } else {
194             return (boolean) @is_writable($strPath);
195         }
196     }
197
198     /**
199      * Whether file can be deleted.
200      * @param File $f
201      * @return boolean
202      */
203     function canDelete(File $f)
204     {
205         clearstatcache();
206          $dir = dirname($f->getAbsolutePath());
207          return (bool) @is_writable($dir);
208     }
209
210     /**
211      * Return the time at which the file or directory denoted by the given
212      * abstract pathname was last modified, or zero if it does not exist or
213      * some other I/O error occurs.
214      */
215     function getLastModifiedTime(File $f) {
216
217         if (!$f->exists()) {
218             return 0;
219         }
220
221         @clearstatcache();
222         $strPath = (string) $f->getPath();
223         $mtime = @filemtime($strPath);
224         if (false === $mtime) {
225             // FAILED. Log and return err.
226             $msg = "FileSystem::Filemtime() FAILED. Cannot can not get modified time of $strPath. $php_errormsg";
227             throw new Exception($msg);
228         } else {
229             return (int) $mtime;
230         }
231     }
232
233     /**
234      * Return the length in bytes of the file denoted by the given abstract
235      * pathname, or zero if it does not exist, is a directory, or some other
236      * I/O error occurs.
237      */
238     function getLength(File $f) {
239         $strPath = (string) $f->getAbsolutePath();
240         $fs = filesize((string) $strPath);
241         if ($fs !== false) {
242             return $fs;
243         } else {
244             $msg = "FileSystem::Read() FAILED. Cannot get filesize of $strPath. $php_errormsg";
245             throw new Exception($msg);
246         }
247     }
248
249     /* -- File operations -- */
250
251     /**
252      * Create a new empty file with the given pathname.  Return
253      * true if the file was created and false if a
254      * file or directory with the given pathname already exists.  Throw an
255      * IOException if an I/O error occurs.
256      *
257      * @param       string      Path of the file to be created.
258      *
259      * @throws      IOException
260      */
261     function createNewFile($strPathname) {
262         if (@file_exists($strPathname))
263             return false;
264
265         // Create new file
266         $fp = @fopen($strPathname, "w");
267         if ($fp === false) {
268             throw new IOException("The file \"$strPathname\" could not be created");
269         }
270         @fclose($fp);
271         return true;
272     }
273
274     /**
275      * Delete the file or directory denoted by the given abstract pathname,
276      * returning true if and only if the operation succeeds.
277      */
278     function delete(File $f) {
279         if ($f->isDirectory()) {
280             return $this->rmdir($f->getPath());
281         } else {
282             return $this->unlink($f->getPath());
283         }
284     }
285
286     /**
287      * Arrange for the file or directory denoted by the given abstract
288      * pathname to be deleted when Phing::shutdown is called, returning
289     * true if and only if the operation succeeds.
290      */
291     function deleteOnExit($f) {
292         throw new Exception("deleteOnExit() not implemented by local fs driver");
293     }
294
295     /**
296      * List the elements of the directory denoted by the given abstract
297      * pathname.  Return an array of strings naming the elements of the
298      * directory if successful; otherwise, return <code>null</code>.
299      */
300     function listDir(File $f) {
301         $strPath = (string) $f->getAbsolutePath();
302         $d = @dir($strPath);
303         if (!$d) {
304             return null;
305         }
306         $list = array();
307         while($entry = $d->read()) {
308             if ($entry != "." && $entry != "..") {
309                 array_push($list, $entry);
310             }
311         }
312         $d->close();
313         unset($d);
314         return $list;
315     }
316
317     /**
318      * Create a new directory denoted by the given abstract pathname,
319      * returning true if and only if the operation succeeds.
320      */
321     function createDirectory(&$f) {
322         return @mkdir($f->getAbsolutePath(),0755);
323     }
324
325     /**
326      * Rename the file or directory denoted by the first abstract pathname to
327      * the second abstract pathname, returning true if and only if
328      * the operation succeeds.
329      *
330      * @param File $f1 abstract source file
331      * @param File $f2 abstract destination file
332      * @return void
333      * @throws Exception if rename cannot be performed
334      */
335     function rename(File $f1, File $f2) {
336         // get the canonical paths of the file to rename
337         $src = $f1->getAbsolutePath();
338         $dest = $f2->getAbsolutePath();
339         if (false === @rename($src, $dest)) {
340             $msg = "Rename FAILED. Cannot rename $src to $dest. $php_errormsg";
341             throw new Exception($msg);
342         }
343     }
344
345     /**
346      * Set the last-modified time of the file or directory denoted by the
347      * given abstract pathname returning true if and only if the
348      * operation succeeds.
349      * @return void
350      * @throws Exception
351      */
352     function setLastModifiedTime(File $f, $time) {
353         $path = $f->getPath();
354         $success = @touch($path, $time);
355         if (!$success) {
356             throw new Exception("Could not create directory due to: $php_errormsg");
357         }
358     }
359
360     /**
361      * Mark the file or directory denoted by the given abstract pathname as
362      * read-only, returning <code>true</code> if and only if the operation
363      * succeeds.
364      */
365     function setReadOnly($f) {
366         throw new Exception("setReadonle() not implemented by local fs driver");
367     }
368
369     /* -- Filesystem interface -- */
370
371     /**
372      * List the available filesystem roots, return array of File objects
373      */
374     function listRoots() {
375         throw new Exception("SYSTEM ERROR [listRoots() not implemented by local fs driver]");
376     }
377
378     /* -- Basic infrastructure -- */
379
380     /**
381      * Compare two abstract pathnames lexicographically.
382      */
383     function compare($f1, $f2) {
384         throw new Exception("SYSTEM ERROR [compare() not implemented by local fs driver]");
385     }
386
387     /**
388      * Copy a file.
389      *
390      * @param File $src Source path and name file to copy.
391      * @param File $dest Destination path and name of new file.
392      *
393      * @return void
394      * @throws Exception if file cannot be copied.
395      */
396     function copy(File $src, File $dest) {
397         global $php_errormsg;
398         $srcPath  = $src->getAbsolutePath();
399         $destPath = $dest->getAbsolutePath();
400
401         if (false === @copy($srcPath, $destPath)) { // Copy FAILED. Log and return err.
402             // Add error from php to end of log message. $php_errormsg.
403             $msg = "FileSystem::copy() FAILED. Cannot copy $srcPath to $destPath. $php_errormsg";
404             throw new Exception($msg);
405         }
406
407         try {
408             $dest->setMode($src->getMode());
409         } catch(Exception $exc) {
410             // [MA] does chmod returns an error on systems that do not support it ?
411             // eat it up for now.
412         }
413     }
414
415     /**
416      * Change the permissions on a file or directory.
417      *
418      * @param    pathname    String. Path and name of file or directory.
419      * @param    mode        Int. The mode (permissions) of the file or
420      *                        directory. If using octal add leading 0. eg. 0777.
421      *                        Mode is affected by the umask system setting.
422      *
423      * @return void
424      * @throws Exception if operation failed.
425      */
426     function chmod($pathname, $mode) {
427         $str_mode = decoct($mode); // Show octal in messages.
428         if (false === @chmod($pathname, $mode)) {// FAILED.
429             $msg = "FileSystem::chmod() FAILED. Cannot chmod $pathname. Mode $str_mode." . (isset($php_errormsg) ? ' ' . $php_errormsg : "");
430             throw new Exception($msg);
431         }
432     }
433
434
435     /**
436          * Change the ownership on a file or directory.
437          *
438          * @param    pathname    String. Path and name of file or directory.
439          * @param    user        String. The user of the file or
440          *                        directory.See http://us.php.net/chown
441          *
442          * @return void
443          * @throws Exception if operation failed.
444          */
445         function chown($pathname, $user) {
446
447             if (false === @chown($pathname, $user)) {// FAILED.
448                 $msg = "FileSystem::chown() FAILED. Cannot chown $pathname. User $user." . (isset($php_errormsg) ? ' ' . $php_errormsg : "");
449                 throw new Exception($msg);
450             }
451     }
452
453     /**
454      * Locks a file and throws an Exception if this is not possible.
455      * @return void
456      * @throws Exception
457      */
458     function lock(File $f) {
459         $filename = $f->getPath();
460         $fp = @fopen($filename, "w");
461         $result = @flock($fp, LOCK_EX);
462         @fclose($fp);
463         if (!$result) {
464             throw new Exception("Could not lock file '$filename'");
465         }
466     }
467
468     /**
469      * Unlocks a file and throws an IO Error if this is not possible.
470      *
471      * @throws Exception
472      * @return void
473      */
474     function unlock(File $f) {
475         $filename = $f->getPath();
476         $fp = @fopen($filename, "w");
477         $result = @flock($fp, LOCK_UN);
478         fclose($fp);
479         if (!$result) {
480             throw new Exception("Could not unlock file '$filename'");
481         }
482     }
483
484     /**
485      * Delete a file.
486      *
487      * @param    file    String. Path and/or name of file to delete.
488      *
489      * @return void
490      * @throws Exception - if an error is encountered.
491      */
492     function unlink($file) {
493         global $php_errormsg;
494         if (false === @unlink($file)) {
495             $msg = "FileSystem::unlink() FAILED. Cannot unlink '$file'. $php_errormsg";
496             throw new Exception($msg);
497         }
498     }
499
500     /**
501      * Symbolically link a file to another name.
502      *
503      * Currently symlink is not implemented on Windows. Don't use if the application is to be portable.
504      *
505      * @param string $target Path and/or name of file to link.
506      * @param string $link Path and/or name of link to be created.
507      * @return void
508      */
509     function symlink($target, $link) {
510
511         // If Windows OS then symlink() will report it is not supported in
512         // the build. Use this error instead of checking for Windows as the OS.
513
514         if (false === @symlink($target, $link)) {
515             // Add error from php to end of log message. $php_errormsg.
516             $msg = "FileSystem::Symlink() FAILED. Cannot symlink '$target' to '$link'. $php_errormsg";
517             throw new Exception($msg);
518         }
519
520     }
521
522     /**
523      * Set the modification and access time on a file to the present time.
524      *
525      * @param string $file Path and/or name of file to touch.
526      * @param int $time
527      * @return void
528      */
529     function touch($file, $time = null) {
530         global $php_errormsg;
531
532         if (null === $time) {
533             $error = @touch($file);
534         } else {
535             $error = @touch($file, $time);
536         }
537
538         if (false === $error) { // FAILED.
539             // Add error from php to end of log message. $php_errormsg.
540             $msg = "FileSystem::touch() FAILED. Cannot touch '$file'. $php_errormsg";
541             throw new Exception($msg);
542         }
543     }
544
545     /**
546      * Delete an empty directory OR a directory and all of its contents.
547      *
548      * @param    dir    String. Path and/or name of directory to delete.
549      * @param    children    Boolean.    False: don't delete directory contents.
550      *                                    True: delete directory contents.
551      *
552      * @return void
553      */
554     function rmdir($dir, $children = false) {
555         global $php_errormsg;
556
557         // If children=FALSE only delete dir if empty.
558         if (false === $children) {
559
560             if (false === @rmdir($dir)) { // FAILED.
561                 // Add error from php to end of log message. $php_errormsg.
562                 $msg = "FileSystem::rmdir() FAILED. Cannot rmdir $dir. $php_errormsg";
563                 throw new Exception($msg);
564             }
565
566         } else { // delete contents and dir.
567
568             $handle = @opendir($dir);
569
570             if (false === $handle) { // Error.
571
572                 $msg = "FileSystem::rmdir() FAILED. Cannot opendir() $dir. $php_errormsg";
573                 throw new Exception($msg);
574
575             } else { // Read from handle.
576
577                 // Don't error on readdir().
578                 while (false !== ($entry = @readdir($handle))) {
579
580                     if ($entry != '.' && $entry != '..') {
581
582                         // Only add / if it isn't already the last char.
583                         // This ONLY serves the purpose of making the Logger
584                         // output look nice:)
585
586                         if (strpos(strrev($dir), DIRECTORY_SEPARATOR) === 0) {// there is a /
587                             $next_entry = $dir . $entry;
588                         } else { // no /
589                             $next_entry = $dir . DIRECTORY_SEPARATOR . $entry;
590                         }
591
592                         // NOTE: As of php 4.1.1 is_dir doesn't return FALSE it
593                         // returns 0. So use == not ===.
594
595                         // Don't error on is_dir()
596                         if (false == @is_dir($next_entry)) { // Is file.
597
598                             try {
599                                 self::unlink($next_entry); // Delete.
600                             } catch (Exception $e) {
601                                 $msg = "FileSystem::Rmdir() FAILED. Cannot FileSystem::Unlink() $next_entry. ". $e->getMessage();
602                                 throw new Exception($msg);
603                             }
604
605                         } else { // Is directory.
606
607                             try {
608                                 self::rmdir($next_entry, true); // Delete
609                             } catch (Exception $e) {
610                                 $msg = "FileSystem::rmdir() FAILED. Cannot FileSystem::rmdir() $next_entry. ". $e->getMessage();
611                                 throw new Exception($msg);
612                             }
613
614                         } // end is_dir else
615                     } // end .. if
616                 } // end while
617             } // end handle if
618
619             // Don't error on closedir()
620             @closedir($handle);
621
622             if (false === @rmdir($dir)) { // FAILED.
623                 // Add error from php to end of log message. $php_errormsg.
624                 $msg = "FileSystem::rmdir() FAILED. Cannot rmdir $dir. $php_errormsg";
625                 throw new Exception($msg);
626             }
627
628         }
629
630     }
631
632     /**
633      * Set the umask for file and directory creation.
634      *
635      * @param    mode    Int. Permissions ususally in ocatal. Use leading 0 for
636    &nb