0011984: printing events with tags is broken for daysview
[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 (! Tinebase_Core::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         // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
376         if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
377             $updatedFileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
378             header('X-OC-MTime: accepted');
379             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
380                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$updatedFileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$updatedFileObject->id}");
381
382         }
383         
384         // sanitize file size, somehow filesize() seems to return empty strings on some systems
385         if (empty($updatedFileObject->size)) {
386             $updatedFileObject->size = 0;
387         }
388         
389         return $this->_fileObjectBackend->update($updatedFileObject);
390     }
391     
392     /**
393      * update hash of all directories for given path
394      * 
395      * @param string $path
396      */
397     protected function _updateDirectoryNodesHash($path)
398     {
399         // update hash of all parent folders
400         $parentNodes = $this->_getPathNodes($path);
401         $updatedNodes = $this->_fileObjectBackend->updateDirectoryNodesHash($parentNodes);
402         
403         // update nodes stored in local statCache
404         $subPath = null;
405         foreach ($parentNodes as $node) {
406             $directoryObject = $updatedNodes->getById($node->object_id);
407             
408             if ($directoryObject) {
409                 $node->revision = $directoryObject->revision;
410                 $node->hash     = $directoryObject->hash;
411             }
412             
413             $subPath .= "/" . $node->name;
414             $this->_addStatCache($subPath, $node);
415         }
416     }
417     
418     /**
419      * open file
420      * 
421      * @param string $_path
422      * @param string $_mode
423      * @return handle
424      */
425     public function fopen($_path, $_mode)
426     {
427         $dirName = dirname($_path);
428         $fileName = basename($_path);
429         
430         switch ($_mode) {
431             // Create and open for writing only; place the file pointer at the beginning of the file. 
432             // If the file already exists, the fopen() call will fail by returning FALSE and generating 
433             // an error of level E_WARNING. If the file does not exist, attempt to create it. This is 
434             // equivalent to specifying O_EXCL|O_CREAT flags for the underlying open(2) system call.
435             case 'x':
436             case 'xb':
437                 if (!$this->isDir($dirName) || $this->fileExists($_path)) {
438                     return false;
439                 }
440                 
441                 $parent = $this->stat($dirName);
442                 $node = $this->createFileTreeNode($parent, $fileName);
443                 
444                 $handle = Tinebase_TempFile::getInstance()->openTempFile();
445                 
446                 break;
447                 
448             // Open for reading only; place the file pointer at the beginning of the file.
449             case 'r':
450             case 'rb':
451                 if ($this->isDir($_path) || !$this->fileExists($_path)) {
452                     return false;
453                 }
454                 
455                 $node = $this->stat($_path);
456                 $hashFile = $this->_basePath . '/' . substr($node->hash, 0, 3) . '/' . substr($node->hash, 3);
457                 
458                 $handle = fopen($hashFile, $_mode);
459                 
460                 break;
461                 
462             // Open for writing only; place the file pointer at the beginning of the file and truncate the 
463             // file to zero length. If the file does not exist, attempt to create it.
464             case 'w':
465             case 'wb':
466                 if (!$this->isDir($dirName)) {
467                     return false;
468                 }
469                 
470                 if (!$this->fileExists($_path)) {
471                     $parent = $this->stat($dirName);
472                     $node = $this->createFileTreeNode($parent, $fileName);
473                 } else {
474                     $node = $this->stat($_path);
475                 }
476                 
477                 $handle = Tinebase_TempFile::getInstance()->openTempFile();
478                 
479                 break;
480                 
481             default:
482                 return false;
483         }
484         
485         $contextOptions = array('tine20' => array(
486             'path' => $_path,
487             'mode' => $_mode,
488             'node' => $node
489         ));
490         stream_context_set_option($handle, $contextOptions);
491         
492         return $handle;
493     }
494     
495     /**
496      * get content type
497      * 
498      * @deprecated use Tinebase_FileSystem::stat()->contenttype
499      * @param  string  $path
500      * @return string
501      */
502     public function getContentType($path)
503     {
504         $node = $this->stat($path);
505         
506         return $node->contenttype;
507     }
508     
509     /**
510      * get etag
511      * 
512      * @deprecated use Tinebase_FileSystem::stat()->hash
513      * @param  string $path
514      * @return string
515      */
516     public function getETag($path)
517     {
518         $node = $this->stat($path);
519         
520         return $node->hash;
521     }
522     
523     /**
524      * return if path is a directory
525      * 
526      * @param  string  $path
527      * @return boolean
528      */
529     public function isDir($path)
530     {
531         try {
532             $node = $this->stat($path);
533         } catch (Tinebase_Exception_InvalidArgument $teia) {
534             return false;
535         } catch (Tinebase_Exception_NotFound $tenf) {
536             return false;
537         }
538         
539         if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
540             return false;
541         }
542         
543         return true;
544     }
545     
546     /**
547      * return if path is a file
548      *
549      * @param  string  $path
550      * @return boolean
551      */
552     public function isFile($path)
553     {
554         try {
555             $node = $this->stat($path);
556         } catch (Tinebase_Exception_InvalidArgument $teia) {
557             return false;
558         } catch (Tinebase_Exception_NotFound $tenf) {
559             return false;
560         }
561     
562         if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FILE) {
563             return false;
564         }
565     
566         return true;
567     }
568     
569     /**
570      * rename file/directory
571      *
572      * @param  string  $oldPath
573      * @param  string  $newPath
574      * @return Tinebase_Model_Tree_Node
575      */
576     public function rename($oldPath, $newPath)
577     {
578         try {
579             $node = $this->stat($oldPath);
580         } catch (Tinebase_Exception_InvalidArgument $teia) {
581             return false;
582         } catch (Tinebase_Exception_NotFound $tenf) {
583             return false;
584         }
585     
586         if (dirname($oldPath) != dirname($newPath)) {
587             try {
588                 $newParent = $this->stat(dirname($newPath));
589             } catch (Tinebase_Exception_InvalidArgument $teia) {
590                 return false;
591             } catch (Tinebase_Exception_NotFound $tenf) {
592                 return false;
593             }
594     
595             $node->parent_id = $newParent->getId();
596         }
597     
598         if (basename($oldPath) != basename($newPath)) {
599             $node->name = basename($newPath);
600         }
601     
602         $node = $this->_treeNodeBackend->update($node);
603         
604         $this->clearStatCache($oldPath);
605         
606         $this->_addStatCache($newPath, $node);
607         
608         return $node;
609     }
610     
611     /**
612      * create directory
613      * 
614      * @param string $path
615      */
616     public function mkdir($path)
617     {
618         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
619             . ' Creating directory ' . $path);
620         
621         $currentPath = array();
622         $parentNode  = null;
623         $pathParts   = $this->_splitPath($path);
624         
625         foreach ($pathParts as $pathPart) {
626             $pathPart = trim($pathPart);
627             $currentPath[]= $pathPart;
628             
629             try {
630                 $node = $this->stat('/' . implode('/', $currentPath));
631             } catch (Tinebase_Exception_NotFound $tenf) {
632                 $node = $this->createDirectoryTreeNode($parentNode, $pathPart);
633                 
634                 $this->_addStatCache($currentPath, $node);
635             }
636             
637             $parentNode = $node;
638         }
639         
640         // update hash of all parent folders
641         $this->_updateDirectoryNodesHash($path);
642         
643         return $node;
644     }
645     
646     /**
647      * remove directory
648      * 
649      * @param  string   $path
650      * @param  boolean  $recursive
651      * @return boolean
652      */
653     public function rmdir($path, $recursive = FALSE)
654     {
655         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
656             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Removing directory ' . $path);
657         
658         $node = $this->stat($path);
659         
660         $children = $this->getTreeNodeChildren($node);
661         
662         // check if child entries exists and delete if $_recursive is true
663         if (count($children) > 0) {
664             if ($recursive !== true) {
665                 throw new Tinebase_Exception_InvalidArgument('directory not empty');
666             } else {
667                 foreach ($children as $child) {
668                     if ($this->isDir($path . '/' . $child->name)) {
669                         $this->rmdir($path . '/' . $child->name, true);
670                     } else {
671                         $this->unlink($path . '/' . $child->name);
672                     }
673                 }
674             }
675         }
676         
677         $this->_treeNodeBackend->delete($node->getId());
678         $this->clearStatCache($path);
679
680         // delete object only, if no other tree node refers to it
681         if ($this->_treeNodeBackend->getObjectCount($node->object_id) == 0) {
682             $this->_fileObjectBackend->delete($node->object_id);
683         }
684         
685         return true;
686     }
687     
688     /**
689      * scan dir
690      * 
691      * @param  string  $path
692      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
693      */
694     public function scanDir($path)
695     {
696         $children = $this->getTreeNodeChildren($this->stat($path));
697         
698         foreach ($children as $node) {
699             $this->_addStatCache($path . '/' . $node->name, $node);
700         }
701         
702         return $children;
703     }
704     
705     /**
706      * @param  string  $path
707      * @return Tinebase_Model_Tree_Node
708      */
709     public function stat($path)
710     {
711         $pathParts = $this->_splitPath($path);
712         $cacheId = $this->_getCacheId($pathParts);
713         
714         // let's see if the path is cached in statCache
715         if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
716             try {
717                 // let's try to get the node from backend, to make sure it still exists
718                 return $this->_treeNodeBackend->get($this->_statCache[$cacheId]);
719             } catch (Tinebase_Exception_NotFound $tenf) {
720                 // something went wrong. let's clear the whole statCache
721                 $this->clearStatCache();
722             }
723         }
724         
725         $parentNode = null;
726         $node       = null;
727         
728         // find out if we have cached any node up in the path
729         while (($pathPart = array_pop($pathParts) !== null)) {
730             $cacheId = $this->_getCacheId($pathParts);
731             
732             if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
733                 $parentNode = $this->_statCache[$cacheId];
734                 break;
735             }
736         }
737         
738         $missingPathParts = array_diff_assoc($this->_splitPath($path), $pathParts);
739         
740         foreach ($missingPathParts as $pathPart) {
741             $node = $this->_treeNodeBackend->getChild($parentNode, $pathPart);
742             
743             // keep track of current path position
744             array_push($pathParts, $pathPart);
745             
746             // add found path to statCache
747             $this->_addStatCache($pathParts, $node);
748             
749             $parentNode = $node;
750         }
751         
752         return $node;
753     }
754     
755     /**
756      * get filesize
757      * 
758      * @deprecated use Tinebase_FileSystem::stat()->size
759      * @param  string  $path
760      * @return integer
761      */
762     public function filesize($path)
763     {
764         $node = $this->stat($path);
765         
766         return $node->size;
767     }
768     
769     /**
770      * delete file
771      * 
772      * @param  string  $_path
773      * @return boolean
774      */
775     public function unlink($path)
776     {
777         $node = $this->stat($path);
778         $this->deleteFileNode($node);
779         
780         $this->clearStatCache($path);
781         
782         // update hash of all parent folders
783         $this->_updateDirectoryNodesHash(dirname($path));
784         
785         return true;
786     }
787     
788     /**
789      * delete file node
790      * 
791      * @param Tinebase_Model_Tree_Node $node
792      */
793     public function deleteFileNode(Tinebase_Model_Tree_Node $node)
794     {
795         if ($node->type == Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
796             throw new Tinebase_Exception_InvalidArgument('can not unlink directories');
797         }
798         
799         $this->_treeNodeBackend->delete($node->getId());
800         
801         // delete object only, if no one uses it anymore
802         if ($this->_treeNodeBackend->getObjectCount($node->object_id) == 0) {
803             $this->_fileObjectBackend->delete($node->object_id);
804         }
805     }
806     
807     /**
808      * create directory
809      * 
810      * @param  string|Tinebase_Model_Tree_Node  $parentId
811      * @param  string                           $name
812      * @return Tinebase_Model_Tree_Node
813      */
814     public function createDirectoryTreeNode($parentId, $name)
815     {
816         $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
817         
818         $directoryObject = new Tinebase_Model_Tree_FileObject(array(
819             'type'          => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
820             'contentytype'  => null,
821             'hash'          => Tinebase_Record_Abstract::generateUID(),
822             'size'          => 0
823         ));
824         Tinebase_Timemachine_ModificationLog::setRecordMetaData($directoryObject, 'create');
825         $directoryObject = $this->_fileObjectBackend->create($directoryObject);
826         
827         $treeNode = new Tinebase_Model_Tree_Node(array(
828             'name'          => $name,
829             'object_id'     => $directoryObject->getId(),
830             'parent_id'     => $parentId
831         ));
832         $treeNode = $this->_treeNodeBackend->create($treeNode);
833         
834         return $treeNode;
835     }
836     
837     /**
838      * create new file node
839      * 
840      * @param  string|Tinebase_Model_Tree_Node  $parentId
841      * @param  string                           $name
842      * @throws Tinebase_Exception_InvalidArgument
843      * @return Tinebase_Model_Tree_Node
844      */
845     public function createFileTreeNode($parentId, $name)
846     {
847         $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
848         
849         $fileObject = new Tinebase_Model_Tree_FileObject(array(
850             'type'          => Tinebase_Model_Tree_FileObject::TYPE_FILE,
851             'contentytype'  => null,
852         ));
853         Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'create');
854
855         // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
856         if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
857             $fileObject->creation_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
858             $fileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
859             header('X-OC-MTime: accepted');
860             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
861                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$fileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$name}");
862
863         }
864
865         $fileObject = $this->_fileObjectBackend->create($fileObject);
866
867         $treeNode = new Tinebase_Model_Tree_Node(array(
868             'name'          => $name,
869             'object_id'     => $fileObject->getId(),
870             'parent_id'     => $parentId
871         ));
872         
873         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
874             ' ' . print_r($treeNode->toArray(), TRUE));
875         
876         $treeNode = $this->_treeNodeBackend->create($treeNode);
877
878         return $treeNode;
879     }
880     
881     /**
882      * places contents into a file blob
883      * 
884      * @param  stream|string|tempFile $contents
885      * @return string hash
886      */
887     public function createFileBlob($contents)
888     {
889         if (! is_resource($contents)) {
890             throw new Tinebase_Exception_NotImplemented('please implement me!');
891         }
892         
893         $handle = $contents;
894         rewind($handle);
895         
896         $ctx = hash_init('sha1');
897         hash_update_stream($ctx, $handle);
898         $hash = hash_final($ctx);
899         
900         $hashDirectory = $this->_basePath . '/' . substr($hash, 0, 3);
901         if (!file_exists($hashDirectory)) {
902             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash directory: ' . $hashDirectory);
903             if(mkdir($hashDirectory, 0700) === false) {
904                 throw new Tinebase_Exception_UnexpectedValue('failed to create directory');
905             }
906         }
907         
908         $hashFile      = $hashDirectory . '/' . substr($hash, 3);
909         if (!file_exists($hashFile)) {
910             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash file: ' . $hashFile);
911             rewind($handle);
912             $hashHandle = fopen($hashFile, 'x');
913             stream_copy_to_stream($handle, $hashHandle);
914             fclose($hashHandle);
915         }
916         
917         return array($hash, $hashFile);
918     }
919     
920     /**
921      * get tree node children
922      * 
923      * @param string|Tinebase_Model_Tree_Node|Tinebase_Record_RecordSet  $nodeId
924      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
925      */
926     public function getTreeNodeChildren($nodeId)
927     {
928         if ($nodeId instanceof Tinebase_Model_Tree_Node) {
929             $nodeId = $nodeId->getId();
930             $operator = 'equals';
931         } elseif ($nodeId instanceof Tinebase_Record_RecordSet) {
932             $nodeId = $nodeId->getArrayOfIds();
933             $operator = 'in';
934         } else {
935             $operator = 'equals';
936         }
937         
938         $searchFilter = new Tinebase_Model_Tree_Node_Filter(array(
939             array(
940                 'field'     => 'parent_id',
941                 'operator'  => $operator,
942                 'value'     => $nodeId
943             )
944         ));
945         $children = $this->searchNodes($searchFilter);
946         
947         return $children;
948     }
949     
950     /**
951      * search tree nodes
952      * 
953      * @param Tinebase_Model_Tree_Node_Filter $_filter
954      * @param Tinebase_Record_Interface $_pagination
955      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
956      */
957     public function searchNodes(Tinebase_Model_Tree_Node_Filter $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL)
958     {
959         $result = $this->_treeNodeBackend->search($_filter, $_pagination);
960         return $result;
961     }
962     
963     /**
964     * search tree nodes count
965     *
966     * @param Tinebase_Model_Tree_Node_Filter $_filter
967     * @return integer
968     */
969     public function searchNodesCount(Tinebase_Model_Tree_Node_Filter $_filter = NULL)
970     {
971         $result = $this->_treeNodeBackend->searchCount($_filter);
972         return $result;
973     }
974     
975     /**
976      * get nodes by container (or container id)
977      * 
978      * @param int|Tinebase_Model_Container $container
979      * @return Tinebase_Record_RecordSet
980      */
981     public function getNodesByContainer($container)
982     {
983         $nodeContainer = ($container instanceof Tinebase_Model_Container) ? $container : Tinebase_Container::getInstance()->getContainerById($container);
984         $path = $this->getContainerPath($nodeContainer);
985         $parentNode = $this->stat($path);
986         $filter = new Tinebase_Model_Tree_Node_Filter(array(
987             array('field' => 'parent_id', 'operator' => 'equals', 'value' => $parentNode->getId())
988         ));
989         
990         return $this->searchNodes($filter);
991     }
992     
993     /**
994      * get tree node specified by parent node (or id) and name
995      * 
996      * @param string|Tinebase_Model_Tree_Node $_parentId
997      * @param string $_name
998      * @throws Tinebase_Exception_InvalidArgument
999      * @return Tinebase_Model_Tree_Node
1000      */
1001     public function getTreeNode($_parentId, $_name)
1002     {
1003         $parentId = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId->getId() : $_parentId;
1004         
1005         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1006             . ' Getting tree node ' . $parentId . '/'. $_name);
1007         
1008         return $this->_treeNodeBackend->getChild($_parentId, $_name);
1009     }
1010     
1011     /**
1012      * add entry to stat cache
1013      * 
1014      * @param string|array              $path
1015      * @param Tinebase_Model_Tree_Node  $node
1016      */
1017     protected function _addStatCache($path, Tinebase_Model_Tree_Node $node)
1018     {
1019         $this->_statCache[$this->_getCacheId($path)] = $node;
1020     }
1021     
1022     /**
1023      * generate cache id
1024      * 
1025      * @param  string|array  $path
1026      * @return string
1027      */
1028     protected function _getCacheId($path) 
1029     {
1030         $pathParts = is_array($path) ? $path : $this->_splitPath($path);
1031         
1032         return sha1(implode(null, $pathParts));
1033     }
1034     
1035     /**
1036      * split path
1037      * 
1038      * @param  string  $path
1039      * @return array
1040      */
1041     protected function _splitPath($path)
1042     {
1043         return explode('/', trim($path, '/'));
1044     }
1045     
1046     /**
1047      * update node
1048      * 
1049      * @param Tinebase_Model_Tree_Node $_node
1050      * @return Tinebase_Model_Tree_Node
1051      */
1052     public function update(Tinebase_Model_Tree_Node $_node)
1053     {
1054         $currentNodeObject = $this->get($_node->getId());
1055         $fileObject = $this->_fileObjectBackend->get($currentNodeObject->object_id);
1056
1057         Tinebase_Timemachine_ModificationLog::setRecordMetaData($_node, 'update', $currentNodeObject);
1058         Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'update', $fileObject);
1059
1060         // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
1061         if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
1062             $fileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1063             header('X-OC-MTime: accepted');
1064             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
1065                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$fileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$_node->name}");
1066
1067         }
1068
1069         // update file object
1070         $fileObject->description = $_node->description;
1071         $this->_updateFileObject($fileObject, $_node->hash);
1072         
1073         return $this->_treeNodeBackend->update($_node);
1074     }
1075     
1076     /**
1077      * get container of node
1078      * 
1079      * @param Tinebase_Model_Tree_Node|string $node
1080      * @return Tinebase_Model_Container
1081      */
1082     public function getNodeContainer($node)
1083     {
1084         $nodesPath = $this->getPathOfNode($node);
1085         
1086         if (count($nodesPath) < 4) {
1087             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
1088                 ' ' . print_r($nodesPath[0], TRUE));
1089             throw new Tinebase_Exception_NotFound('Could not find container for node ' . $nodesPath[0]['id']);
1090         }
1091         
1092         $containerNode = ($nodesPath[2]['name'] === Tinebase_Model_Container::TYPE_PERSONAL) ? $nodesPath[4] : $nodesPath[3];
1093         return Tinebase_Container::getInstance()->get($containerNode['name']);
1094     }
1095     
1096     /**
1097      * get path of node
1098      * 
1099      * @param Tinebase_Model_Tree_Node|string $node
1100      * @param boolean $getPathAsString
1101      * @return array|string
1102      */
1103     public function getPathOfNode($node, $getPathAsString = FALSE)
1104     {
1105         $node = $node instanceof Tinebase_Model_Tree_Node ? $node : $this->get($node);
1106         
1107         $nodesPath = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($node));
1108         while ($node->parent_id) {
1109             $node = $this->get($node->parent_id);
1110             $nodesPath->addRecord($node);
1111         }
1112         
1113         $result = ($getPathAsString) ? '/' . implode('/', array_reverse($nodesPath->name)) : array_reverse($nodesPath->toArray());
1114         return $result;
1115     }
1116     
1117     protected function _getPathNodes($path)
1118     {
1119         $pathParts = $this->_splitPath($path);
1120         
1121         if (empty($pathParts)) {
1122             throw new Tinebase_Exception_InvalidArgument('empty path provided');
1123         }
1124         
1125         $subPath   = null;
1126         $pathNodes = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
1127         
1128         foreach ($pathParts as $pathPart) {
1129             $subPath .= "/$pathPart"; 
1130             
1131             $node = $this->stat($subPath);
1132             if ($node) {
1133                 $pathNodes->addRecord($node);
1134             }
1135         }
1136         
1137         return $pathNodes;
1138     }
1139     
1140     /**
1141      * clears deleted files from filesystem + database
1142      */
1143     public function clearDeletedFiles()
1144     {
1145         $this->clearDeletedFilesFromFilesystem();
1146         $this->clearDeletedFilesFromDatabase();
1147     }
1148     
1149     /**
1150      * removes deleted files that no longer exist in the database from the filesystem
1151      * 
1152      * @return integer number of deleted files
1153      */
1154     public function clearDeletedFilesFromFilesystem()
1155     {
1156         try {
1157             $dirIterator = new DirectoryIterator($this->_basePath);
1158         } catch (Exception $e) {
1159             throw new Tinebase_Exception_AccessDenied('Could not open files directory.');
1160         }
1161         
1162         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1163             . ' Scanning ' . $this->_basePath . ' for deleted files ...');
1164         
1165         $deleteCount = 0;
1166         foreach ($dirIterator as $item) {
1167             $subDir = $item->getFileName();
1168             if ($subDir[0] == '.') continue;
1169             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1170                 . ' Checking ' . $subDir);
1171             $subDirIterator = new DirectoryIterator($this->_basePath . '/' . $subDir);
1172             $hashsToCheck = array();
1173             // loop dirs + check if files in dir are in tree_filerevisions
1174             foreach ($subDirIterator as $file) {
1175                 if ($file->isFile()) {
1176                     $hash = $subDir . $file->getFileName();
1177                     $hashsToCheck[] = $hash;
1178                 }
1179             }
1180             $existingHashes = $this->_fileObjectBackend->checkRevisions($hashsToCheck);
1181             $hashesToDelete = array_diff($hashsToCheck, $existingHashes);
1182             // remove from filesystem if not existing any more
1183             foreach ($hashesToDelete as $hashToDelete) {
1184                 $filename = $this->_basePath . '/' . $subDir . '/' . substr($hashToDelete, 3);
1185                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1186                     . ' Deleting ' . $filename);
1187                 unlink($filename);
1188                 $deleteCount++;
1189             }
1190         }
1191         
1192         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1193             . ' Deleted ' . $deleteCount . ' obsolete file(s).');
1194         
1195         return $deleteCount;
1196     }
1197     
1198     /**
1199      * removes deleted files that no longer exist in the filesystem from the database
1200      * 
1201      * @return integer number of deleted files
1202      */
1203     public function clearDeletedFilesFromDatabase()
1204     {
1205         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1206             . ' Scanning database for deleted files ...');
1207         
1208         // get all file objects from db and check filesystem existance
1209         $toDeleteIds = array();
1210         $fileObjects = $this->_fileObjectBackend->getAll();
1211         foreach ($fileObjects as $fileObject) {
1212             if ($fileObject->type == Tinebase_Model_Tree_FileObject::TYPE_FILE && $fileObject->hash && ! file_exists($fileObject->getFilesystemPath())) {
1213                 $toDeleteIds[] = $fileObject->getId();
1214             }
1215         }
1216         
1217         $nodeIdsToDelete = $this->_treeNodeBackend->search(new Tinebase_Model_Tree_Node_Filter(array(array(
1218             'field'     => 'object_id',
1219             'operator'  => 'in',
1220             'value'     => $toDeleteIds
1221         ))), NULL, Tinebase_Backend_Sql_Abstract::IDCOL);
1222         
1223         $deleteCount = $this->_treeNodeBackend->delete($nodeIdsToDelete);
1224         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1225             . ' Removed ' . $deleteCount . ' obsolete filenode(s) from the database.');
1226         
1227         return $deleteCount;
1228     }
1229
1230     /**
1231      * copy tempfile data to file path
1232      * 
1233      * @param  mixed   $tempFile
1234          Tinebase_Model_Tree_Node     with property hash, tempfile or stream
1235          Tinebase_Model_Tempfile      tempfile
1236          string                       with tempFile id
1237          array                        with [id] => tempFile id (this is odd IMHO)
1238          stream                       stream ressource
1239          NULL                         create empty file
1240      * @param  string  $path
1241      * @throws Tinebase_Exception_AccessDenied
1242      */
1243     public function copyTempfile($tempFile, $path)
1244     {
1245         if ($tempFile === NULL) {
1246             $tempStream = fopen('php://memory', 'r');
1247         } else if (is_resource($tempFile)) {
1248             $tempStream = $tempFile;
1249         } else if (is_string($tempFile) || is_array($tempFile)) {
1250             $tempFile = Tinebase_TempFile::getInstance()->getTempFile($tempFile);
1251             return $this->copyTempfile($tempFile, $path);
1252         } else if ($tempFile instanceof Tinebase_Model_Tree_Node) {
1253             if (isset($tempFile->hash)) {
1254                 $hashFile = $this->_basePath . '/' . substr($tempFile->hash, 0, 3) . '/' . substr($tempFile->hash, 3);
1255                 $tempStream = fopen($hashFile, 'r');
1256             } else if (is_resource($tempFile->stream)) {
1257                 $tempStream = $tempFile->stream;
1258             } else {
1259                 return $this->copyTempfile($tempFile->tempFile, $path);
1260             }
1261         } else if ($tempFile instanceof Tinebase_Model_TempFile) {
1262             $tempStream = fopen($tempFile->path, 'r');
1263         } else {
1264             throw new Tinebase_Exception_UnexpectedValue('unexpected tempfile value');
1265         }
1266         
1267         return $this->copyStream($tempStream, $path);
1268     }
1269     
1270     /**
1271      * copy stream data to file path
1272      *
1273      * @param  stream  $in
1274      * @param  string  $path
1275      * @throws Tinebase_Exception_AccessDenied
1276      * @throws Tinebase_Exception_UnexpectedValue
1277      */
1278     public function copyStream($in, $path)
1279     {
1280         if (! $handle = $this->fopen($path, 'w')) {
1281             throw new Tinebase_Exception_AccessDenied('Permission denied to create file (filename ' . $path . ')');
1282         }
1283         
1284         if (! is_resource($in)) {
1285             throw new Tinebase_Exception_UnexpectedValue('source needs to be of type stream');
1286         }
1287         
1288         if (is_resource($in) !== NULL) {
1289             $metaData = stream_get_meta_data($in);
1290             if (true === $metaData['seekable']) {
1291                 rewind($in);
1292             }
1293             stream_copy_to_stream($in, $handle);
1294             
1295             $this->clearStatCache($path);
1296         }
1297         
1298         $this->fclose($handle);
1299     }
1300 }