43aea8ec642ed49c42ab886dc1f39daa32574fa7
[tine20] / tine20 / Tinebase / FileSystem.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  FileSystem
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2010-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  * 
11  * @todo 0007376: Tinebase_FileSystem / Node model refactoring: move all container related functionality to Filemanager
12  */
13
14 /**
15  * filesystem controller
16  *
17  * @package     Tinebase
18  * @subpackage  FileSystem
19  */
20 class Tinebase_FileSystem implements Tinebase_Controller_Interface
21 {
22     /**
23      * folder name/type for record attachments
24      * 
25      * @var string
26      */
27     const FOLDER_TYPE_RECORDS = 'records';
28     
29     /**
30      * @var Tinebase_Tree_FileObject
31      */
32     protected $_fileObjectBackend;
33     
34     /**
35      * @var Tinebase_Tree_Node
36      */
37     protected $_treeNodeBackend;
38     
39     /**
40      * path where physical files gets stored
41      * 
42      * @var string
43      */
44     protected $_basePath;
45     
46     /**
47      * stat cache
48      * 
49      * @var array
50      */
51     protected $_statCache = array();
52     
53     /**
54      * holds the instance of the singleton
55      *
56      * @var Tinebase_FileSystem
57      */
58     private static $_instance = NULL;
59     
60     /**
61      * the constructor
62      */
63     public function __construct() 
64     {
65         $this->_fileObjectBackend  = new Tinebase_Tree_FileObject();
66         $this->_treeNodeBackend    = new Tinebase_Tree_Node();
67         
68         if (! Setup_Controller::getInstance()->isFilesystemAvailable()) {
69             throw new Tinebase_Exception_Backend('No base path (filesdir) configured or path not writeable');
70         }
71         
72         $this->_basePath = Tinebase_Core::getConfig()->filesdir;
73     }
74     
75     /**
76      * the singleton pattern
77      *
78      * @return Tinebase_FileSystem
79      */
80     public static function getInstance() 
81     {
82         if (self::$_instance === NULL) {
83             self::$_instance = new Tinebase_FileSystem;
84         }
85         
86         return self::$_instance;
87     }
88     
89     /**
90      * init application base paths
91      * 
92      * @param Tinebase_Model_Application|string $_application
93      */
94     public function initializeApplication($_application)
95     {
96         // create app root node
97         $appPath = $this->getApplicationBasePath($_application);
98         if (!$this->fileExists($appPath)) {
99             $this->mkdir($appPath);
100         }
101         
102         $sharedBasePath = $this->getApplicationBasePath($_application, Tinebase_Model_Container::TYPE_SHARED);
103         if (!$this->fileExists($sharedBasePath)) {
104             $this->mkdir($sharedBasePath);
105         }
106         
107         $personalBasePath = $this->getApplicationBasePath($_application, Tinebase_Model_Container::TYPE_PERSONAL);
108         if (!$this->fileExists($personalBasePath)) {
109             $this->mkdir($personalBasePath);
110         }
111     }
112     
113     /**
114      * get application base path
115      * 
116      * @param Tinebase_Model_Application|string $_application
117      * @param string $_type
118      * @return string
119      */
120     public function getApplicationBasePath($_application, $_type = NULL)
121     {
122         $application = $_application instanceof Tinebase_Model_Application 
123             ? $_application 
124             : Tinebase_Application::getInstance()->getApplicationById($_application);
125         
126         $result = '/' . $application->getId();
127         
128         if ($_type !== NULL) {
129             if (! in_array($_type, array(Tinebase_Model_Container::TYPE_SHARED, Tinebase_Model_Container::TYPE_PERSONAL, self::FOLDER_TYPE_RECORDS))) {
130                 throw new Tinebase_Exception_UnexpectedValue('Type can only be shared or personal.');
131             }
132             
133             $result .= '/folders/' . $_type;
134         }
135         
136         return $result;
137     } 
138     
139     /**
140      * Get one tree node (by id)
141      *
142      * @param integer|Tinebase_Record_Interface $_id
143      * @param $_getDeleted get deleted records
144      * @return Tinebase_Model_Tree_Node
145      */
146     public function get($_id, $_getDeleted = FALSE)
147     {
148         $node = $this->_treeNodeBackend->get($_id, $_getDeleted);
149         $fileObject = $this->_fileObjectBackend->get($node->object_id);
150         $node->description = $fileObject->description;
151         
152         return $node;
153     }
154     
155     /**
156      * Get multiple tree nodes identified by id
157      *
158      * @param string|array $_id Ids
159      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
160      */
161     public function getMultipleTreeNodes($_id) 
162     {
163         return $this->_treeNodeBackend->getMultiple($_id);
164     }
165     
166     /**
167      * create container node
168      * 
169      * @param Tinebase_Model_Container $container
170      */
171     public function createContainerNode(Tinebase_Model_Container $container)
172     {
173         $path = $this->getContainerPath($container);
174         
175         if (!$this->fileExists($path)) {
176             $this->mkdir($path);
177         }
178     }
179
180     /**
181      * get container path
182      * 
183      * @param Tinebase_Model_Container $container
184      * @return string
185      */
186     public function getContainerPath(Tinebase_Model_Container $container)
187     {
188         $treeNodePath = new Tinebase_Model_Tree_Node_Path(array(
189             'application' => Tinebase_Application::getInstance()->getApplicationById($container->application_id)
190         ));
191         $treeNodePath->setContainer($container);
192         
193         return $treeNodePath->statpath;
194     }
195     
196     /**
197      * clear stat cache
198      * 
199      * @param string $path if given, only remove this path from statcache
200      */
201     public function clearStatCache($path = NULL)
202     {
203         if ($path !== NULL) {
204             unset($this->_statCache[$this->_getCacheId($path)]);
205         } else {
206             // clear the whole cache
207             $this->_statCache = array();
208         }
209     }
210     
211     /**
212      * copy file/directory
213      * 
214      * @todo copy recursive
215      * 
216      * @param  string  $sourcePath
217      * @param  string  $destinationPath
218      * @throws Tinebase_Exception_UnexpectedValue
219      * @return Tinebase_Model_Tree_Node
220      */
221     public function copy($sourcePath, $destinationPath)
222     {
223         $destinationNode = $this->stat($sourcePath);
224         $sourcePathParts = $this->_splitPath($sourcePath);
225         
226         try {
227             // does destinationPath exist ...
228             $parentNode = $this->stat($destinationPath);
229             
230             // ... and is a directory?
231             if (! $parentNode->type == Tinebase_Model_Tree_Node::TYPE_FOLDER) {
232                 throw new Tinebase_Exception_UnexpectedValue("Destination path exists and is a file. Please remove before.");
233             }
234             
235             $destinationNodeName  = basename(trim($sourcePath, '/'));
236             $destinationPathParts = array_merge($this->_splitPath($destinationPath), (array)$destinationNodeName);
237
238         } catch (Tinebase_Exception_NotFound $tenf) {
239             // does parent directory of destinationPath exist?
240             try {
241                 $parentNode = $this->stat(dirname($destinationPath));
242             } catch (Tinebase_Exception_NotFound $tenf) {
243                 throw new Tinebase_Exception_UnexpectedValue("Parent directory does not exist. Please create before.");
244             }
245             
246             $destinationNodeName = basename(trim($destinationPath, '/'));
247             $destinationPathParts = array_merge($this->_splitPath(dirname($destinationPath)), (array)$destinationNodeName);
248         }
249         
250         if ($sourcePathParts == $destinationPathParts) {
251             throw new Tinebase_Exception_UnexpectedValue("Source path and destination path must be different.");
252         }
253         
254         // set new node properties
255         $destinationNode->setId(null);
256         $destinationNode->parent_id = $parentNode->getId();
257         $destinationNode->name      = $destinationNodeName;
258         
259         $createdNode = $this->_treeNodeBackend->create($destinationNode);
260         
261         // update hash of all parent folders
262         $this->_updateDirectoryNodesHash(dirname(implode('/', $destinationPathParts)));
263         
264         return $createdNode;
265     }
266     
267     /**
268      * get modification timestamp
269      * 
270      * @param  string  $path
271      * @return string  UNIX timestamp
272      */
273     public function getMTime($path)
274     {
275         $node = $this->stat($path);
276         
277         $timestamp = $node->last_modified_time instanceof Tinebase_DateTime 
278             ? $node->last_modified_time->getTimestamp() 
279             : $node->creation_time->getTimestamp();
280         
281         return $timestamp;
282     }
283     
284     /**
285      * check if file exists
286      * 
287      * @param  string $path
288      * @return boolean true if file/directory exists
289      */
290     public function fileExists($path) 
291     {
292         try {
293             $this->stat($path);
294         } catch (Tinebase_Exception_NotFound $tenf) {
295             return false;
296         }
297         
298         return true;
299     }
300     
301     /**
302      * close file handle
303      * 
304      * @param  handle $handle
305      * @return boolean
306      */
307     public function fclose($handle)
308     {
309         if (!is_resource($handle)) {
310             return false;
311         }
312         
313         $options = stream_context_get_options($handle);
314         
315         switch ($options['tine20']['mode']) {
316             case 'w':
317             case 'wb':
318             case 'x':
319             case 'xb':
320                 list ($hash, $hashFile) = $this->createFileBlob($handle);
321                 
322                 $this->_updateFileObject($options['tine20']['node']->object_id, $hash, $hashFile);
323                 
324                 $this->clearStatCache($options['tine20']['path']);
325                 
326                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Writing to file : ' . $options['tine20']['path'] . ' successful.');
327                 
328                 break;
329                 
330             default:
331                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Got mode : ' . $options['tine20']['mode'] . ' - nothing to do.');
332         }
333         
334         fclose($handle);
335         
336         // update hash of all parent folders
337         $this->_updateDirectoryNodesHash(dirname($options['tine20']['path']));
338         
339         return true;
340     }
341     
342     /**
343      * update file object with hash file info
344      * 
345      * @param string $_id
346      * @param string $_hash
347      * @param string $_hashFile
348      * @return Tinebase_Model_Tree_FileObject
349      */
350     protected function _updateFileObject($_id, $_hash, $_hashFile = null)
351     {
352         $currentFileObject = $_id instanceof Tinebase_Record_Abstract ? $_id : $this->_fileObjectBackend->get($_id);
353         
354         $_hashFile = $_hashFile ?: ($this->_basePath . '/' . substr($_hash, 0, 3) . '/' . substr($_hash, 3));
355         
356         $updatedFileObject = clone($currentFileObject);
357         $updatedFileObject->hash = $_hash;
358         $updatedFileObject->size = filesize($_hashFile);
359         
360         if (version_compare(PHP_VERSION, '5.3.0', '>=') && function_exists('finfo_open')) {
361             $finfo = finfo_open(FILEINFO_MIME_TYPE);
362             $mimeType = finfo_file($finfo, $_hashFile);
363             if ($mimeType !== false) {
364                 $updatedFileObject->contenttype = $mimeType;
365             }
366             finfo_close($finfo);
367         } else {
368             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
369                 . ' finfo_open() is not available: Could not get file information.');
370         }
371         
372         $modLog = Tinebase_Timemachine_ModificationLog::getInstance();
373         $modLog->setRecordMetaData($updatedFileObject, 'update', $currentFileObject);
374         
375         // sanitize file size, somehow filesize() seems to return empty strings on some systems
376         if (empty($updatedFileObject->size)) {
377             $updatedFileObject->size = 0;
378         }
379         
380         return $this->_fileObjectBackend->update($updatedFileObject);
381     }
382     
383     /**
384      * update hash of all directories for given path
385      * 
386      * @param string $path
387      */
388     protected function _updateDirectoryNodesHash($path)
389     {
390         // update hash of all parent folders
391         $parentNodes = $this->_getPathNodes($path);
392         $updatedNodes = $this->_fileObjectBackend->updateDirectoryNodesHash($parentNodes);
393         
394         // update nodes stored in local statCache
395         $subPath = null;
396         foreach ($parentNodes as $node) {
397             $directoryObject = $updatedNodes->getById($node->object_id);
398             
399             if ($directoryObject) {
400                 $node->revision = $directoryObject->revision;
401                 $node->hash     = $directoryObject->hash;
402             }
403             
404             $subPath .= "/" . $node->name;
405             $this->_addStatCache($subPath, $node);
406         }
407     }
408     
409     /**
410      * open file
411      * 
412      * @param string $_path
413      * @param string $_mode
414      * @return handle
415      */
416     public function fopen($_path, $_mode)
417     {
418         $dirName = dirname($_path);
419         $fileName = basename($_path);
420         
421         switch ($_mode) {
422             // Create and open for writing only; place the file pointer at the beginning of the file. 
423             // If the file already exists, the fopen() call will fail by returning FALSE and generating 
424             // an error of level E_WARNING. If the file does not exist, attempt to create it. This is 
425             // equivalent to specifying O_EXCL|O_CREAT flags for the underlying open(2) system call.
426             case 'x':
427             case 'xb':
428                 if (!$this->isDir($dirName) || $this->fileExists($_path)) {
429                     return false;
430                 }
431                 
432                 $parent = $this->stat($dirName);
433                 $node = $this->createFileTreeNode($parent, $fileName);
434                 
435                 $handle = Tinebase_TempFile::getInstance()->openTempFile();
436                 
437                 break;
438                 
439             // Open for reading only; place the file pointer at the beginning of the file.
440             case 'r':
441             case 'rb':
442                 if ($this->isDir($_path) || !$this->fileExists($_path)) {
443                     return false;
444                 }
445                 
446                 $node = $this->stat($_path);
447                 $hashFile = $this->_basePath . '/' . substr($node->hash, 0, 3) . '/' . substr($node->hash, 3);
448                 
449                 $handle = fopen($hashFile, $_mode);
450                 
451                 break;
452                 
453             // Open for writing only; place the file pointer at the beginning of the file and truncate the 
454             // file to zero length. If the file does not exist, attempt to create it.
455             case 'w':
456             case 'wb':
457                 if (!$this->isDir($dirName)) {
458                     return false;
459                 }
460                 
461                 if (!$this->fileExists($_path)) {
462                     $parent = $this->stat($dirName);
463                     $node = $this->createFileTreeNode($parent, $fileName);
464                 } else {
465                     $node = $this->stat($_path);
466                 }
467                 
468                 $handle = Tinebase_TempFile::getInstance()->openTempFile();
469                 
470                 break;
471                 
472             default:
473                 return false;
474         }
475         
476         $contextOptions = array('tine20' => array(
477             'path' => $_path,
478             'mode' => $_mode,
479             'node' => $node
480         ));
481         stream_context_set_option($handle, $contextOptions);
482         
483         return $handle;
484     }
485     
486     /**
487      * get content type
488      * 
489      * @deprecated use Tinebase_FileSystem::stat()->contenttype
490      * @param  string  $path
491      * @return string
492      */
493     public function getContentType($path)
494     {
495         $node = $this->stat($path);
496         
497         return $node->contenttype;
498     }
499     
500     /**
501      * get etag
502      * 
503      * @deprecated use Tinebase_FileSystem::stat()->hash
504      * @param  string $path
505      * @return string
506      */
507     public function getETag($path)
508     {
509         $node = $this->stat($path);
510         
511         return $node->hash;
512     }
513     
514     /**
515      * return if path is a directory
516      * 
517      * @param  string  $path
518      * @return boolean
519      */
520     public function isDir($path)
521     {
522         try {
523             $node = $this->stat($path);
524         } catch (Tinebase_Exception_InvalidArgument $teia) {
525             return false;
526         } catch (Tinebase_Exception_NotFound $tenf) {
527             return false;
528         }
529         
530         if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
531             return false;
532         }
533         
534         return true;
535     }
536     
537     /**
538      * return if path is a file
539      *
540      * @param  string  $path
541      * @return boolean
542      */
543     public function isFile($path)
544     {
545         try {
546             $node = $this->stat($path);
547         } catch (Tinebase_Exception_InvalidArgument $teia) {
548             return false;
549         } catch (Tinebase_Exception_NotFound $tenf) {
550             return false;
551         }
552     
553         if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FILE) {
554             return false;
555         }
556     
557         return true;
558     }
559     
560     /**
561      * rename file/directory
562      *
563      * @param  string  $oldPath
564      * @param  string  $newPath
565      * @return Tinebase_Model_Tree_Node
566      */
567     public function rename($oldPath, $newPath)
568     {
569         try {
570             $node = $this->stat($oldPath);
571         } catch (Tinebase_Exception_InvalidArgument $teia) {
572             return false;
573         } catch (Tinebase_Exception_NotFound $tenf) {
574             return false;
575         }
576     
577         if (dirname($oldPath) != dirname($newPath)) {
578             try {
579                 $newParent = $this->stat(dirname($newPath));
580             } catch (Tinebase_Exception_InvalidArgument $teia) {
581                 return false;
582             } catch (Tinebase_Exception_NotFound $tenf) {
583                 return false;
584             }
585     
586             $node->parent_id = $newParent->getId();
587         }
588     
589         if (basename($oldPath) != basename($newPath)) {
590             $node->name = basename($newPath);
591         }
592     
593         $node = $this->_treeNodeBackend->update($node);
594         
595         $this->clearStatCache($oldPath);
596         
597         $this->_addStatCache($newPath, $node);
598         
599         return $node;
600     }
601     
602     /**
603      * create directory
604      * 
605      * @param string $path
606      */
607     public function mkdir($path)
608     {
609         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
610             . ' Creating directory ' . $path);
611         
612         $currentPath = array();
613         $parentNode  = null;
614         $pathParts   = $this->_splitPath($path);
615         
616         foreach ($pathParts as $pathPart) {
617             $pathPart = trim($pathPart);
618             $currentPath[]= $pathPart;
619             
620             try {
621                 $node = $this->stat('/' . implode('/', $currentPath));
622             } catch (Tinebase_Exception_NotFound $tenf) {
623                 $node = $this->createDirectoryTreeNode($parentNode, $pathPart);
624                 
625                 $this->_addStatCache($currentPath, $node);
626             }
627             
628             $parentNode = $node;
629         }
630         
631         // update hash of all parent folders
632         $this->_updateDirectoryNodesHash($path);
633         
634         return $node;
635     }
636     
637     /**
638      * remove directory
639      * 
640      * @param  string   $path
641      * @param  boolean  $recursive
642      * @return boolean
643      */
644     public function rmdir($path, $recursive = FALSE)
645     {
646         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
647             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Removing directory ' . $path);
648         
649         $node = $this->stat($path);
650         
651         $children = $this->getTreeNodeChildren($node);
652         
653         // check if child entries exists and delete if $_recursive is true
654         if (count($children) > 0) {
655             if ($recursive !== true) {
656                 throw new Tinebase_Exception_InvalidArgument('directory not empty');
657             } else {
658                 foreach ($children as $child) {
659                     if ($this->isDir($path . '/' . $child->name)) {
660                         $this->rmdir($path . '/' . $child->name, true);
661                     } else {
662                         $this->unlink($path . '/' . $child->name);
663                     }
664                 }
665             }
666         }
667         
668         $this->_treeNodeBackend->delete($node->getId());
669         $this->clearStatCache($path);
670
671         // delete object only, if no other tree node refers to it
672         if ($this->_treeNodeBackend->getObjectCount($node->object_id) == 0) {
673             $this->_fileObjectBackend->delete($node->object_id);
674         }
675         
676         return true;
677     }
678     
679     /**
680      * scan dir
681      * 
682      * @param  string  $path
683      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
684      */
685     public function scanDir($path)
686     {
687         $children = $this->getTreeNodeChildren($this->stat($path));
688         
689         foreach ($children as $node) {
690             $this->_addStatCache($path . '/' . $node->name, $node);
691         }
692         
693         return $children;
694     }
695     
696     /**
697      * @param  string  $path
698      * @return Tinebase_Model_Tree_Node
699      */
700     public function stat($path)
701     {
702         $pathParts = $this->_splitPath($path);
703         $cacheId = $this->_getCacheId($pathParts);
704         
705         // let's see if the path is cached in statCache
706         if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
707             try {
708                 // let's try to get the node from backend, to make sure it still exists
709                 return $this->_treeNodeBackend->get($this->_statCache[$cacheId]);
710             } catch (Tinebase_Exception_NotFound $tenf) {
711                 // something went wrong. let's clear the whole statCache
712                 $this->clearStatCache();
713             }
714         }
715         
716         $parentNode = null;
717         $node       = null;
718         
719         // find out if we have cached any node up in the path
720         while (($pathPart = array_pop($pathParts) !== null)) {
721             $cacheId = $this->_getCacheId($pathParts);
722             
723             if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
724                 $parentNode = $this->_statCache[$cacheId];
725                 break;
726             }
727         }
728         
729         $missingPathParts = array_diff($this->_splitPath($path), $pathParts);
730         
731         foreach ($missingPathParts as $pathPart) {
732             $node = $this->_treeNodeBackend->getChild($parentNode, $pathPart);
733             
734             // keep track of current path posistion
735             array_push($pathParts, $pathPart);
736             
737             // add found path to statCache
738             $this->_addStatCache($pathParts, $node);
739             
740             $parentNode = $node;
741         }
742         
743         return $node;
744     }
745     
746     /**
747      * get filesize
748      * 
749      * @deprecated use Tinebase_FileSystem::stat()->size
750      * @param  string  $path
751      * @return integer
752      */
753     public function filesize($path)
754     {
755         $node = $this->stat($path);
756         
757         return $node->size;
758     }
759     
760     /**
761      * delete file
762      * 
763      * @param  string  $_path
764      * @return boolean
765      */
766     public function unlink($path)
767     {
768         $node = $this->stat($path);
769         $this->deleteFileNode($node);
770         
771         $this->clearStatCache($path);
772         
773         // update hash of all parent folders
774         $this->_updateDirectoryNodesHash(dirname($path));
775         
776         return true;
777     }
778     
779     /**
780      * delete file node
781      * 
782      * @param Tinebase_Model_Tree_Node $node
783      */
784     public function deleteFileNode(Tinebase_Model_Tree_Node $node)
785     {
786         if ($node->type == Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
787             throw new Tinebase_Exception_InvalidArgument('can not unlink directories');
788         }
789         
790         $this->_treeNodeBackend->delete($node->getId());
791         
792         // delete object only, if no one uses it anymore
793         if ($this->_treeNodeBackend->getObjectCount($node->object_id) == 0) {
794             $this->_fileObjectBackend->delete($node->object_id);
795         }
796     }
797     
798     /**
799      * create directory
800      * 
801      * @param  string|Tinebase_Model_Tree_Node  $parentId
802      * @param  string                           $name
803      * @return Tinebase_Model_Tree_Node
804      */
805     public function createDirectoryTreeNode($parentId, $name)
806     {
807         $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
808         
809         $directoryObject = new Tinebase_Model_Tree_FileObject(array(
810             'type'          => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
811             'contentytype'  => null,
812             'hash'          => Tinebase_Record_Abstract::generateUID(),
813             'size'          => 0
814         ));
815         Tinebase_Timemachine_ModificationLog::setRecordMetaData($directoryObject, 'create');
816         $directoryObject = $this->_fileObjectBackend->create($directoryObject);
817         
818         $treeNode = new Tinebase_Model_Tree_Node(array(
819             'name'          => $name,
820             'object_id'     => $directoryObject->getId(),
821             'parent_id'     => $parentId
822         ));
823         $treeNode = $this->_treeNodeBackend->create($treeNode);
824         
825         return $treeNode;
826     }
827     
828     /**
829      * create new file node
830      * 
831      * @param  string|Tinebase_Model_Tree_Node  $parentId
832      * @param  string                           $name
833      * @throws Tinebase_Exception_InvalidArgument
834      * @return Tinebase_Model_Tree_Node
835      */
836     public function createFileTreeNode($parentId, $name)
837     {
838         $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
839         
840         $fileObject = new Tinebase_Model_Tree_FileObject(array(
841             'type'          => Tinebase_Model_Tree_FileObject::TYPE_FILE,
842             'contentytype'  => null,
843         ));
844         Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'create');
845         $fileObject = $this->_fileObjectBackend->create($fileObject);
846         
847         $treeNode = new Tinebase_Model_Tree_Node(array(
848             'name'          => $name,
849             'object_id'     => $fileObject->getId(),
850             'parent_id'     => $parentId
851         ));
852         
853         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
854             ' ' . print_r($treeNode->toArray(), TRUE));
855         
856         $treeNode = $this->_treeNodeBackend->create($treeNode);
857         
858         return $treeNode;
859     }
860     
861     /**
862      * places contents into a file blob
863      * 
864      * @param  stream|string|tempFile $contents
865      * @return string hash
866      */
867     public function createFileBlob($contents)
868     {
869         if (! is_resource($contents)) {
870             throw new Tinebase_Exception_NotImplemented('please implement me!');
871         }
872         
873         $handle = $contents;
874         rewind($handle);
875         
876         $ctx = hash_init('sha1');
877         hash_update_stream($ctx, $handle);
878         $hash = hash_final($ctx);
879         
880         $hashDirectory = $this->_basePath . '/' . substr($hash, 0, 3);
881         if (!file_exists($hashDirectory)) {
882             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash directory: ' . $hashDirectory);
883             if(mkdir($hashDirectory, 0700) === false) {
884                 throw new Tinebase_Exception_UnexpectedValue('failed to create directory');
885             }
886         }
887         
888         $hashFile      = $hashDirectory . '/' . substr($hash, 3);
889         if (!file_exists($hashFile)) {
890             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash file: ' . $hashFile);
891             rewind($handle);
892             $hashHandle = fopen($hashFile, 'x');
893             stream_copy_to_stream($handle, $hashHandle);
894             fclose($hashHandle);
895         }
896         
897         return array($hash, $hashFile);
898     }
899     
900     /**
901      * get tree node children
902      * 
903      * @param string|Tinebase_Model_Tree_Node|Tinebase_Record_RecordSet  $nodeId
904      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
905      */
906     public function getTreeNodeChildren($nodeId)
907     {
908         if ($nodeId instanceof Tinebase_Model_Tree_Node) {
909             $nodeId = $nodeId->getId();
910             $operator = 'equals';
911         } elseif ($nodeId instanceof Tinebase_Record_RecordSet) {
912             $nodeId = $nodeId->getArrayOfIds();
913             $operator = 'in';
914         } else {
915             $nodeId = $_nodeId;
916             $operator = 'equals';
917         }
918         
919         $searchFilter = new Tinebase_Model_Tree_Node_Filter(array(
920             array(
921                 'field'     => 'parent_id',
922                 'operator'  => $operator,
923                 'value'     => $nodeId
924             )
925         ));
926         $children = $this->searchNodes($searchFilter);
927         
928         return $children;
929     }
930     
931     /**
932      * search tree nodes
933      * 
934      * @param Tinebase_Model_Tree_Node_Filter $_filter
935      * @param Tinebase_Record_Interface $_pagination
936      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
937      */
938     public function searchNodes(Tinebase_Model_Tree_Node_Filter $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL)
939     {
940         $result = $this->_treeNodeBackend->search($_filter, $_pagination);
941         return $result;
942     }
943     
944     /**
945     * search tree nodes count
946     *
947     * @param Tinebase_Model_Tree_Node_Filter $_filter
948     * @return integer
949     */
950     public function searchNodesCount(Tinebase_Model_Tree_Node_Filter $_filter = NULL)
951     {
952         $result = $this->_treeNodeBackend->searchCount($_filter);
953         return $result;
954     }
955     
956     /**
957      * get nodes by container (or container id)
958      * 
959      * @param int|Tinebase_Model_Container $container
960      * @return Tinebase_Record_RecordSet
961      */
962     public function getNodesByContainer($container)
963     {
964         $nodeContainer = ($container instanceof Tinebase_Model_Container) ? $container : Tinebase_Container::getInstance()->getContainerById($container);
965         $path = $this->getContainerPath($nodeContainer);
966         $parentNode = $this->stat($path);
967         $filter = new Tinebase_Model_Tree_Node_Filter(array(
968             array('field' => 'parent_id', 'operator' => 'equals', 'value' => $parentNode->getId())
969         ));
970         
971         return $this->searchNodes($filter);
972     }
973     
974     /**
975      * get tree node specified by parent node (or id) and name
976      * 
977      * @param string|Tinebase_Model_Tree_Node $_parentId
978      * @param string $_name
979      * @throws Tinebase_Exception_InvalidArgument
980      * @return Tinebase_Model_Tree_Node
981      */
982     public function getTreeNode($_parentId, $_name)
983     {
984         $parentId = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId->getId() : $_parentId;
985         
986         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
987             . ' Getting tree node ' . $parentId . '/'. $_name);
988         
989         return $this->_treeNodeBackend->getChild($_parentId, $_name);
990     }
991     
992     /**
993      * add entry to stat cache
994      * 
995      * @param string|array              $path
996      * @param Tinebase_Model_Tree_Node  $node
997      */
998     protected function _addStatCache($path, Tinebase_Model_Tree_Node $node)
999     {
1000         $this->_statCache[$this->_getCacheId($path)] = $node;
1001     }
1002     
1003     /**
1004      * generate cache id
1005      * 
1006      * @param  string|array  $path
1007      * @return string
1008      */
1009     protected function _getCacheId($path) 
1010     {
1011         $pathParts = is_array($path) ? $path : $this->_splitPath($path);
1012         
1013         return sha1(implode(null, $pathParts));
1014     }
1015     
1016     /**
1017      * split path
1018      * 
1019      * @param  string  $path
1020      * @return array
1021      */
1022     protected function _splitPath($path)
1023     {
1024         return explode('/', trim($path, '/'));
1025     }
1026     
1027     /**
1028      * update node
1029      * 
1030      * @param Tinebase_Model_Tree_Node $_node
1031      * @return Tinebase_Model_Tree_Node
1032      */
1033     public function update(Tinebase_Model_Tree_Node $_node)
1034     {
1035         $currentNodeObject = $this->get($_node->getId());
1036         Tinebase_Timemachine_ModificationLog::setRecordMetaData($_node, 'update', $currentNodeObject);
1037         
1038         // update file object
1039         $fileObject = $this->_fileObjectBackend->get($currentNodeObject->object_id);
1040         $fileObject->description = $_node->description;
1041         
1042         $this->_updateFileObject($fileObject, $_node->hash);
1043         
1044         return $this->_treeNodeBackend->update($_node);
1045     }
1046     
1047     /**
1048      * get container of node
1049      * 
1050      * @param Tinebase_Model_Tree_Node|string $node
1051      * @return Tinebase_Model_Container
1052      */
1053     public function getNodeContainer($node)
1054     {
1055         $nodesPath = $this->getPathOfNode($node);
1056         
1057         if (count($nodesPath) < 4) {
1058             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
1059                 ' ' . print_r($nodesPath[0], TRUE));
1060             throw new Tinebase_Exception_NotFound('Could not find container for node ' . $nodesPath[0]['id']);
1061         }
1062         
1063         $containerNode = ($nodesPath[2]['name'] === Tinebase_Model_Container::TYPE_PERSONAL) ? $nodesPath[4] : $nodesPath[3];
1064         return Tinebase_Container::getInstance()->get($containerNode['name']);
1065     }
1066     
1067     /**
1068      * get path of node
1069      * 
1070      * @param Tinebase_Model_Tree_Node|string $node
1071      * @param boolean $getPathAsString
1072      * @return array|string
1073      */
1074     public function getPathOfNode($node, $getPathAsString = FALSE)
1075     {
1076         $node = $node instanceof Tinebase_Model_Tree_Node ? $node : $this->get($node);
1077         
1078         $nodesPath = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($node));
1079         while ($node->parent_id) {
1080             $node = $this->get($node->parent_id);
1081             $nodesPath->addRecord($node);
1082         }
1083         
1084         $result = ($getPathAsString) ? '/' . implode('/', array_reverse($nodesPath->name)) : array_reverse($nodesPath->toArray());
1085         return $result;
1086     }
1087     
1088     protected function _getPathNodes($path)
1089     {
1090         $pathParts = $this->_splitPath($path);
1091         
1092         if (empty($pathParts)) {
1093             throw new Tinebase_Exception_InvalidArgument('empty path provided');
1094         }
1095         
1096         $subPath   = null;
1097         $pathNodes = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
1098         
1099         foreach ($pathParts as $pathPart) {
1100             $subPath .= "/$pathPart"; 
1101             
1102             $node = $this->stat($subPath);
1103             if ($node) {
1104                 $pathNodes->addRecord($node);
1105             }
1106         }
1107         
1108         return $pathNodes;
1109     }
1110     
1111     /**
1112      * clears deleted files from filesystem + database
1113      */
1114     public function clearDeletedFiles()
1115     {
1116         $this->clearDeletedFilesFromFilesystem();
1117         $this->clearDeletedFilesFromDatabase();
1118     }
1119     
1120     /**
1121      * removes deleted files that no longer exist in the database from the filesystem
1122      * 
1123      * @return integer number of deleted files
1124      */
1125     public function clearDeletedFilesFromFilesystem()
1126     {
1127         try {
1128             $dirIterator = new DirectoryIterator($this->_basePath);
1129         } catch (Exception $e) {
1130             throw new Tinebase_Exception_AccessDenied('Could not open files directory.');
1131         }
1132         
1133         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1134             . ' Scanning ' . $this->_basePath . ' for deleted files ...');
1135         
1136         $deleteCount = 0;
1137         foreach ($dirIterator as $item) {
1138             $subDir = $item->getFileName();
1139             if ($subDir[0] == '.') continue;
1140             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1141                 . ' Checking ' . $subDir);
1142             $subDirIterator = new DirectoryIterator($this->_basePath . '/' . $subDir);
1143             $hashsToCheck = array();
1144             // loop dirs + check if files in dir are in tree_filerevisions
1145             foreach ($subDirIterator as $file) {
1146                 if ($file->isFile()) {
1147                     $hash = $subDir . $file->getFileName();
1148                     $hashsToCheck[] = $hash;
1149                 }
1150             }
1151             $existingHashes = $this->_fileObjectBackend->checkRevisions($hashsToCheck);
1152             $hashesToDelete = array_diff($hashsToCheck, $existingHashes);
1153             // remove from filesystem if not existing any more
1154             foreach ($hashesToDelete as $hashToDelete) {
1155                 $filename = $this->_basePath . '/' . $subDir . '/' . substr($hashToDelete, 3);
1156                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1157                     . ' Deleting ' . $filename);
1158                 unlink($filename);
1159                 $deleteCount++;
1160             }
1161         }
1162         
1163         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1164             . ' Deleted ' . $deleteCount . ' obsolete file(s).');
1165         
1166         return $deleteCount;
1167     }
1168     
1169     /**
1170      * removes deleted files that no longer exist in the filesystem from the database
1171      * 
1172      * @return integer number of deleted files
1173      */
1174     public function clearDeletedFilesFromDatabase()
1175     {
1176         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1177             . ' Scanning database for deleted files ...');
1178         
1179         // get all file objects from db and check filesystem existance
1180         $toDeleteIds = array();
1181         $fileObjects = $this->_fileObjectBackend->getAll();
1182         foreach ($fileObjects as $fileObject) {
1183             if ($fileObject->type == Tinebase_Model_Tree_FileObject::TYPE_FILE && $fileObject->hash && ! file_exists($fileObject->getFilesystemPath())) {
1184                 $toDeleteIds[] = $fileObject->getId();
1185             }
1186         }
1187         
1188         $nodeIdsToDelete = $this->_treeNodeBackend->search(new Tinebase_Model_Tree_Node_Filter(array(array(
1189             'field'     => 'object_id',
1190             'operator'  => 'in',
1191             'value'     => $toDeleteIds
1192         ))), NULL, Tinebase_Backend_Sql_Abstract::IDCOL);
1193         
1194         $deleteCount = $this->_treeNodeBackend->delete($nodeIdsToDelete);
1195         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1196             . ' Removed ' . $deleteCount . ' obsolete filenode(s) from the database.');
1197         
1198         return $deleteCount;
1199     }
1200
1201     /**
1202      * copy tempfile data to file path
1203      * 
1204      * @param  mixed   $tempFile
1205          Tinebase_Model_Tree_Node     with property hash, tempfile or stream
1206          Tinebase_Model_Tempfile      tempfile
1207          string                       with tempFile id
1208          array                        with [id] => tempFile id (this is odd IMHO)
1209          stream                       stream ressource
1210          NULL                         create empty file
1211      * @param  string  $path
1212      * @throws Tinebase_Exception_AccessDenied
1213      */
1214     public function copyTempfile($tempFile, $path)
1215     {
1216         if ($tempFile === NULL) {
1217             $tempStream = fopen('php://memory', 'r');
1218         } else if (is_resource($tempFile)) {
1219             $tempStream = $tempFile;
1220         } else if (is_string($tempFile) || is_array($tempFile)) {
1221             $tempFile = Tinebase_TempFile::getInstance()->getTempFile($tempFile);
1222             return $this->copyTempfile($tempFile, $path);
1223         } else if ($tempFile instanceof Tinebase_Model_Tree_Node) {
1224             if (isset($tempFile->hash)) {
1225                 $hashFile = $this->_basePath . '/' . substr($tempFile->hash, 0, 3) . '/' . substr($tempFile->hash, 3);
1226                 $tempStream = fopen($hashFile, 'r');
1227             } else if (is_resource($tempFile->stream)) {
1228                 $tempStream = $tempFile->stream;
1229             } else {
1230                 return $this->copyTempfile($tempFile->tempFile, $path);
1231             }
1232         } else if ($tempFile instanceof Tinebase_Model_TempFile) {
1233             $tempStream = fopen($tempFile->path, 'r');
1234         } else {
1235             throw new Tinebase_Exception_UnexpectedValue('unexpected tempfile value');
1236         }
1237         
1238         return $this->copyStream($tempStream, $path);
1239     }
1240     
1241     /**
1242      * copy stream data to file path
1243      *
1244      * @param  stream  $in
1245      * @param  string  $path
1246      * @throws Tinebase_Exception_AccessDenied
1247      * @throws Tinebase_Exception_UnexpectedValue
1248      */
1249     public function copyStream($in, $path)
1250     {
1251         if (! $handle = $this->fopen($path, 'w')) {
1252             throw new Tinebase_Exception_AccessDenied('Permission denied to create file (filename ' . $path . ')');
1253         }
1254         
1255         if (! is_resource($in)) {
1256             throw new Tinebase_Exception_UnexpectedValue('source needs to be of type stream');
1257         }
1258         
1259         if (is_resource($in) !== NULL) {
1260             $metaData = stream_get_meta_data($in);
1261             if (true === $metaData['seekable']) {
1262                 rewind($in);
1263             }
1264             stream_copy_to_stream($in, $handle);
1265             
1266             $this->clearStatCache($path);
1267         }
1268         
1269         $this->fclose($handle);
1270     }
1271 }