51028371a1cb7a64455c72697616bb5f59829159
[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         
704         $cacheId = $this->_getCacheId($pathParts);
705         
706         // let's see if the path is cached in statCache
707         if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
708             try {
709                 // let's try to get the node from backend, to make sure it still exists
710                 return $this->_treeNodeBackend->get($this->_statCache[$cacheId]);
711             } catch (Tinebase_Exception_NotFound $tenf) {
712                 // something went wrong. let's clear the whole statCache
713                 $this->clearStatCache();
714             }
715         }
716         
717         $parentNode = null;
718         $node       = null;
719         
720         // find out if we have cached any node up in the path
721         while ($pathPart = array_pop($pathParts)) {
722             $cacheId = $this->_getCacheId($pathParts);
723             
724             if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
725                 $parentNode = $this->_statCache[$cacheId];
726                 break;
727             }
728         }
729         
730         $missingPathParts = array_diff($this->_splitPath($path), $pathParts);
731         
732         foreach ($missingPathParts as $pathPart) {
733             $node = $this->_treeNodeBackend->getChild($parentNode, $pathPart);
734             
735             // keep track of current path posistion
736             array_push($pathParts, $pathPart);
737             
738             // add found path to statCache
739             $this->_addStatCache($pathParts, $node);
740             
741             $parentNode = $node;
742         }
743         
744         return $node;
745     }
746     
747     /**
748      * get filesize
749      * 
750      * @deprecated use Tinebase_FileSystem::stat()->size
751      * @param  string  $path
752      * @return integer
753      */
754     public function filesize($path)
755     {
756         $node = $this->stat($path);
757         
758         return $node->size;
759     }
760     
761     /**
762      * delete file
763      * 
764      * @param  string  $_path
765      * @return boolean
766      */
767     public function unlink($path)
768     {
769         $node = $this->stat($path);
770         $this->deleteFileNode($node);
771         
772         $this->clearStatCache($path);
773         
774         // update hash of all parent folders
775         $this->_updateDirectoryNodesHash(dirname($path));
776         
777         return true;
778     }
779     
780     /**
781      * delete file node
782      * 
783      * @param Tinebase_Model_Tree_Node $node
784      */
785     public function deleteFileNode(Tinebase_Model_Tree_Node $node)
786     {
787         if ($node->type == Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
788             throw new Tinebase_Exception_InvalidArgument('can not unlink directories');
789         }
790         
791         $this->_treeNodeBackend->delete($node->getId());
792         
793         // delete object only, if no one uses it anymore
794         if ($this->_treeNodeBackend->getObjectCount($node->object_id) == 0) {
795             $this->_fileObjectBackend->delete($node->object_id);
796         }
797     }
798     
799     /**
800      * create directory
801      * 
802      * @param  string|Tinebase_Model_Tree_Node  $parentId
803      * @param  string                           $name
804      * @return Tinebase_Model_Tree_Node
805      */
806     public function createDirectoryTreeNode($parentId, $name)
807     {
808         $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
809         
810         $directoryObject = new Tinebase_Model_Tree_FileObject(array(
811             'type'          => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
812             'contentytype'  => null,
813             'hash'          => Tinebase_Record_Abstract::generateUID(),
814             'size'          => 0
815         ));
816         Tinebase_Timemachine_ModificationLog::setRecordMetaData($directoryObject, 'create');
817         $directoryObject = $this->_fileObjectBackend->create($directoryObject);
818         
819         $treeNode = new Tinebase_Model_Tree_Node(array(
820             'name'          => $name,
821             'object_id'     => $directoryObject->getId(),
822             'parent_id'     => $parentId
823         ));
824         $treeNode = $this->_treeNodeBackend->create($treeNode);
825         
826         return $treeNode;
827     }
828     
829     /**
830      * create new file node
831      * 
832      * @param  string|Tinebase_Model_Tree_Node  $parentId
833      * @param  string                           $name
834      * @throws Tinebase_Exception_InvalidArgument
835      * @return Tinebase_Model_Tree_Node
836      */
837     public function createFileTreeNode($parentId, $name)
838     {
839         $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
840         
841         $fileObject = new Tinebase_Model_Tree_FileObject(array(
842             'type'          => Tinebase_Model_Tree_FileObject::TYPE_FILE,
843             'contentytype'  => null,
844         ));
845         Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'create');
846         $fileObject = $this->_fileObjectBackend->create($fileObject);
847         
848         $treeNode = new Tinebase_Model_Tree_Node(array(
849             'name'          => $name,
850             'object_id'     => $fileObject->getId(),
851             'parent_id'     => $parentId
852         ));
853         
854         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
855             ' ' . print_r($treeNode->toArray(), TRUE));
856         
857         $treeNode = $this->_treeNodeBackend->create($treeNode);
858         
859         return $treeNode;
860     }
861     
862     /**
863      * places contents into a file blob
864      * 
865      * @param  stream|string|tempFile $contents
866      * @return string hash
867      */
868     public function createFileBlob($contents)
869     {
870         if (! is_resource($contents)) {
871             throw new Tinebase_Exception_NotImplemented('please implement me!');
872         }
873         
874         $handle = $contents;
875         rewind($handle);
876         
877         $ctx = hash_init('sha1');
878         hash_update_stream($ctx, $handle);
879         $hash = hash_final($ctx);
880         
881         $hashDirectory = $this->_basePath . '/' . substr($hash, 0, 3);
882         if (!file_exists($hashDirectory)) {
883             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash directory: ' . $hashDirectory);
884             if(mkdir($hashDirectory, 0700) === false) {
885                 throw new Tinebase_Exception_UnexpectedValue('failed to create directory');
886             }
887         }
888         
889         $hashFile      = $hashDirectory . '/' . substr($hash, 3);
890         if (!file_exists($hashFile)) {
891             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash file: ' . $hashFile);
892             rewind($handle);
893             $hashHandle = fopen($hashFile, 'x');
894             stream_copy_to_stream($handle, $hashHandle);
895             fclose($hashHandle);
896         }
897         
898         return array($hash, $hashFile);
899     }
900     
901     /**
902      * get tree node children
903      * 
904      * @param string|Tinebase_Model_Tree_Node|Tinebase_Record_RecordSet  $nodeId
905      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
906      */
907     public function getTreeNodeChildren($nodeId)
908     {
909         if ($nodeId instanceof Tinebase_Model_Tree_Node) {
910             $nodeId = $nodeId->getId();
911             $operator = 'equals';
912         } elseif ($nodeId instanceof Tinebase_Record_RecordSet) {
913             $nodeId = $nodeId->getArrayOfIds();
914             $operator = 'in';
915         } else {
916             $nodeId = $_nodeId;
917             $operator = 'equals';
918         }
919         
920         $searchFilter = new Tinebase_Model_Tree_Node_Filter(array(
921             array(
922                 'field'     => 'parent_id',
923                 'operator'  => $operator,
924                 'value'     => $nodeId
925             )
926         ));
927         $children = $this->searchNodes($searchFilter);
928         
929         return $children;
930     }
931     
932     /**
933      * search tree nodes
934      * 
935      * @param Tinebase_Model_Tree_Node_Filter $_filter
936      * @param Tinebase_Record_Interface $_pagination
937      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
938      */
939     public function searchNodes(Tinebase_Model_Tree_Node_Filter $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL)
940     {
941         $result = $this->_treeNodeBackend->search($_filter, $_pagination);
942         return $result;
943     }
944     
945     /**
946     * search tree nodes count
947     *
948     * @param Tinebase_Model_Tree_Node_Filter $_filter
949     * @return integer
950     */
951     public function searchNodesCount(Tinebase_Model_Tree_Node_Filter $_filter = NULL)
952     {
953         $result = $this->_treeNodeBackend->searchCount($_filter);
954         return $result;
955     }
956     
957     /**
958      * get nodes by container (or container id)
959      * 
960      * @param int|Tinebase_Model_Container $container
961      * @return Tinebase_Record_RecordSet
962      */
963     public function getNodesByContainer($container)
964     {
965         $nodeContainer = ($container instanceof Tinebase_Model_Container) ? $container : Tinebase_Container::getInstance()->getContainerById($container);
966         $path = $this->getContainerPath($nodeContainer);
967         $parentNode = $this->stat($path);
968         $filter = new Tinebase_Model_Tree_Node_Filter(array(
969             array('field' => 'parent_id', 'operator' => 'equals', 'value' => $parentNode->getId())
970         ));
971         
972         return $this->searchNodes($filter);
973     }
974     
975     /**
976      * get tree node specified by parent node (or id) and name
977      * 
978      * @param string|Tinebase_Model_Tree_Node $_parentId
979      * @param string $_name
980      * @throws Tinebase_Exception_InvalidArgument
981      * @return Tinebase_Model_Tree_Node
982      */
983     public function getTreeNode($_parentId, $_name)
984     {
985         $parentId = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId->getId() : $_parentId;
986         
987         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
988             . ' Getting tree node ' . $parentId . '/'. $_name);
989         
990         return $this->_treeNodeBackend->getChild($_parentId, $_name);
991     }
992     
993     /**
994      * add entry to stat cache
995      * 
996      * @param string|array              $path
997      * @param Tinebase_Model_Tree_Node  $node
998      */
999     protected function _addStatCache($path, Tinebase_Model_Tree_Node $node)
1000     {
1001         $this->_statCache[$this->_getCacheId($path)] = $node;
1002     }
1003     
1004     /**
1005      * generate cache id
1006      * 
1007      * @param  string|array  $path
1008      * @return string
1009      */
1010     protected function _getCacheId($path) 
1011     {
1012         $pathParts = is_array($path) ? $path : $this->_splitPath($path);
1013         
1014         return sha1(implode(null, $pathParts));
1015     }
1016     
1017     /**
1018      * split path
1019      * 
1020      * @param  string  $path
1021      * @return array
1022      */
1023     protected function _splitPath($path)
1024     {
1025         return explode('/', trim($path, '/'));
1026     }
1027     
1028     /**
1029      * update node
1030      * 
1031      * @param Tinebase_Model_Tree_Node $_node
1032      * @return Tinebase_Model_Tree_Node
1033      */
1034     public function update(Tinebase_Model_Tree_Node $_node)
1035     {
1036         $currentNodeObject = $this->get($_node->getId());
1037         Tinebase_Timemachine_ModificationLog::setRecordMetaData($_node, 'update', $currentNodeObject);
1038         
1039         // update file object
1040         $fileObject = $this->_fileObjectBackend->get($currentNodeObject->object_id);
1041         $fileObject->description = $_node->description;
1042         
1043         $this->_updateFileObject($fileObject, $_node->hash);
1044         
1045         return $this->_treeNodeBackend->update($_node);
1046     }
1047     
1048     /**
1049      * get container of node
1050      * 
1051      * @param Tinebase_Model_Tree_Node|string $node
1052      * @return Tinebase_Model_Container
1053      */
1054     public function getNodeContainer($node)
1055     {
1056         $nodesPath = $this->getPathOfNode($node);
1057         
1058         if (count($nodesPath) < 4) {
1059             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
1060                 ' ' . print_r($nodesPath[0], TRUE));
1061             throw new Tinebase_Exception_NotFound('Could not find container for node ' . $nodesPath[0]['id']);
1062         }
1063         
1064         $containerNode = ($nodesPath[2]['name'] === Tinebase_Model_Container::TYPE_PERSONAL) ? $nodesPath[4] : $nodesPath[3];
1065         return Tinebase_Container::getInstance()->get($containerNode['name']);
1066     }
1067     
1068     /**
1069      * get path of node
1070      * 
1071      * @param Tinebase_Model_Tree_Node|string $node
1072      * @param boolean $getPathAsString
1073      * @return array|string
1074      */
1075     public function getPathOfNode($node, $getPathAsString = FALSE)
1076     {
1077         $node = $node instanceof Tinebase_Model_Tree_Node ? $node : $this->get($node);
1078         
1079         $nodesPath = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($node));
1080         while ($node->parent_id) {
1081             $node = $this->get($node->parent_id);
1082             $nodesPath->addRecord($node);
1083         }
1084         
1085         $result = ($getPathAsString) ? '/' . implode('/', array_reverse($nodesPath->name)) : array_reverse($nodesPath->toArray());
1086         return $result;
1087     }
1088     
1089     protected function _getPathNodes($path)
1090     {
1091         $pathParts = $this->_splitPath($path);
1092         
1093         if (empty($pathParts)) {
1094             throw new Tinebase_Exception_InvalidArgument('empty path provided');
1095         }
1096         
1097         $subPath   = null;
1098         $pathNodes = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
1099         
1100         foreach ($pathParts as $pathPart) {
1101             $subPath .= "/$pathPart"; 
1102             
1103             $pathNodes->addRecord($this->stat($subPath));
1104         }
1105         
1106         return $pathNodes;
1107     }
1108     
1109     /**
1110      * clears deleted files from filesystem + database
1111      */
1112     public function clearDeletedFiles()
1113     {
1114         $this->clearDeletedFilesFromFilesystem();
1115         $this->clearDeletedFilesFromDatabase();
1116     }
1117     
1118     /**
1119      * removes deleted files that no longer exist in the database from the filesystem
1120      * 
1121      * @return integer number of deleted files
1122      */
1123     public function clearDeletedFilesFromFilesystem()
1124     {
1125         try {
1126             $dirIterator = new DirectoryIterator($this->_basePath);
1127         } catch (Exception $e) {
1128             throw new Tinebase_Exception_AccessDenied('Could not open files directory.');
1129         }
1130         
1131         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1132             . ' Scanning ' . $this->_basePath . ' for deleted files ...');
1133         
1134         $deleteCount = 0;
1135         foreach ($dirIterator as $item) {
1136             $subDir = $item->getFileName();
1137             if ($subDir[0] == '.') continue;
1138             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1139                 . ' Checking ' . $subDir);
1140             $subDirIterator = new DirectoryIterator($this->_basePath . '/' . $subDir);
1141             $hashsToCheck = array();
1142             // loop dirs + check if files in dir are in tree_filerevisions
1143             foreach ($subDirIterator as $file) {
1144                 if ($file->isFile()) {
1145                     $hash = $subDir . $file->getFileName();
1146                     $hashsToCheck[] = $hash;
1147                 }
1148             }
1149             $existingHashes = $this->_fileObjectBackend->checkRevisions($hashsToCheck);
1150             $hashesToDelete = array_diff($hashsToCheck, $existingHashes);
1151             // remove from filesystem if not existing any more
1152             foreach ($hashesToDelete as $hashToDelete) {
1153                 $filename = $this->_basePath . '/' . $subDir . '/' . substr($hashToDelete, 3);
1154                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1155                     . ' Deleting ' . $filename);
1156                 unlink($filename);
1157                 $deleteCount++;
1158             }
1159         }
1160         
1161         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1162             . ' Deleted ' . $deleteCount . ' obsolete file(s).');
1163         
1164         return $deleteCount;
1165     }
1166     
1167     /**
1168      * removes deleted files that no longer exist in the filesystem from the database
1169      * 
1170      * @return integer number of deleted files
1171      */
1172     public function clearDeletedFilesFromDatabase()
1173     {
1174         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1175             . ' Scanning database for deleted files ...');
1176         
1177         // get all file objects from db and check filesystem existance
1178         $toDeleteIds = array();
1179         $fileObjects = $this->_fileObjectBackend->getAll();
1180         foreach ($fileObjects as $fileObject) {
1181             if ($fileObject->type == Tinebase_Model_Tree_FileObject::TYPE_FILE && $fileObject->hash && ! file_exists($fileObject->getFilesystemPath())) {
1182                 $toDeleteIds[] = $fileObject->getId();
1183             }
1184         }
1185         
1186         $nodeIdsToDelete = $this->_treeNodeBackend->search(new Tinebase_Model_Tree_Node_Filter(array(array(
1187             'field'     => 'object_id',
1188             'operator'  => 'in',
1189             'value'     => $toDeleteIds
1190         ))), NULL, Tinebase_Backend_Sql_Abstract::IDCOL);
1191         
1192         $deleteCount = $this->_treeNodeBackend->delete($nodeIdsToDelete);
1193         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1194             . ' Removed ' . $deleteCount . ' obsolete filenode(s) from the database.');
1195         
1196         return $deleteCount;
1197     }
1198
1199     /**
1200      * copy tempfile data to file path
1201      * 
1202      * @param  mixed   $tempFile
1203          Tinebase_Model_Tree_Node     with property hash, tempfile or stream
1204          Tinebase_Model_Tempfile      tempfile
1205          string                       with tempFile id
1206          array                        with [id] => tempFile id (this is odd IMHO)
1207          stream                       stream ressource
1208          NULL                         create empty file
1209      * @param  string  $path
1210      * @throws Tinebase_Exception_AccessDenied
1211      */
1212     public function copyTempfile($tempFile, $path)
1213     {
1214         if ($tempFile === NULL) {
1215             $tempStream = fopen('php://memory', 'r');
1216         }
1217         
1218         else if (is_resource($tempFile)) {
1219             $tempStream = $tempFile;
1220         }
1221         
1222         else if (is_string($tempFile) || is_array($tempFile)) {
1223             $tempFile = Tinebase_TempFile::getInstance()->getTempFile($tempFile);
1224             return $this->copyTempfile($tempFile, $path);
1225         }
1226         
1227         else if ($tempFile instanceof Tinebase_Model_Tree_Node) {
1228             if (isset($tempFile->hash)) {
1229                 $hashFile = $this->_basePath . '/' . substr($tempFile->hash, 0, 3) . '/' . substr($tempFile->hash, 3);
1230                 $tempStream = fopen($hashFile, 'r');
1231             } else if (is_resource($tempFile->stream)) {
1232                 $tempStream = $tempFile->stream;
1233             } else {
1234                 return $this->copyTempfile($tempFile->tempFile, $path);
1235             }
1236         }
1237         
1238         else if ($tempFile instanceof Tinebase_Model_TempFile) {
1239             $tempStream = fopen($tempFile->path, 'r');
1240         }
1241         
1242         else {
1243             throw new Tasks_Exception_UnexpectedValue('unexpected tempfile value');
1244         }
1245         
1246         return $this->copyStream($tempStream, $path);
1247     }
1248     
1249     /**
1250      * copy stream data to file path
1251      *
1252      * @param  stream  $in
1253      * @param  string  $path
1254      * @throws Tinebase_Exception_AccessDenied
1255      * @throws Tinebase_Exception_UnexpectedValue
1256      */
1257     public function copyStream($in, $path)
1258     {
1259         if (! $handle = $this->fopen($path, 'w')) {
1260             throw new Tinebase_Exception_AccessDenied('Permission denied to create file (filename ' . $path . ')');
1261         }
1262         
1263         if (! is_resource($in)) {
1264             throw new Tinebase_Exception_UnexpectedValue('source needs to be of type stream');
1265         }
1266         
1267         if (is_resource($in) !== NULL) {
1268             $metaData = stream_get_meta_data($in);
1269             if (true === $metaData['seekable']) {
1270                 rewind($in);
1271             }
1272             stream_copy_to_stream($in, $handle);
1273             
1274             $this->clearStatCache($path);
1275         }
1276         
1277         $this->fclose($handle);
1278     }
1279 }