0012932: file picker dialog in fileuploadgrid
[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 = null;
38
39     protected $_treeNodeModel = 'Tinebase_Model_Tree_Node';
40     
41     /**
42      * path where physical files gets stored
43      * 
44      * @var string
45      */
46     protected $_basePath;
47
48     protected $_modLogActive = false;
49
50     protected $_indexingActive = false;
51
52     /**
53      * stat cache
54      * 
55      * @var array
56      */
57     protected $_statCache = array();
58     
59     /**
60      * holds the instance of the singleton
61      *
62      * @var Tinebase_FileSystem
63      */
64     private static $_instance = NULL;
65     
66     /**
67      * the constructor
68      */
69     public function __construct() 
70     {
71         if (! Tinebase_Core::isFilesystemAvailable()) {
72             throw new Tinebase_Exception_Backend('No base path (filesdir) configured or path not writeable');
73         }
74
75         $config = Tinebase_Core::getConfig();
76         $this->_modLogActive = true === $config->{Tinebase_Config::FILESYSTEM}->{Tinebase_Config::FILESYSTEM_MODLOGACTIVE};
77         $this->_indexingActive = true === $config->{Tinebase_Config::FILESYSTEM}->{Tinebase_Config::FILESYSTEM_INDEX_CONTENT};
78
79         $this->_fileObjectBackend  = new Tinebase_Tree_FileObject(null, array(
80             Tinebase_Config::FILESYSTEM_MODLOGACTIVE => $this->_modLogActive
81         ));
82
83         $this->_basePath = $config->{Tinebase_Config::FILESDIR};
84     }
85     
86     /**
87      * the singleton pattern
88      *
89      * @return Tinebase_FileSystem
90      */
91     public static function getInstance() 
92     {
93         if (self::$_instance === NULL) {
94             self::$_instance = new Tinebase_FileSystem;
95         }
96         
97         return self::$_instance;
98     }
99
100     public function resetBackends()
101     {
102         $config = Tinebase_Core::getConfig()->{Tinebase_Config::FILESYSTEM};
103         $this->_modLogActive = true === $config->{Tinebase_Config::FILESYSTEM_MODLOGACTIVE};
104         $this->_indexingActive = true === $config->{Tinebase_Config::FILESYSTEM_INDEX_CONTENT};
105
106         $this->_treeNodeBackend = null;
107
108         $this->_fileObjectBackend  = new Tinebase_Tree_FileObject(null, array(
109             Tinebase_Config::FILESYSTEM_MODLOGACTIVE => $this->_modLogActive
110         ));
111     }
112
113     /**
114      * init application base paths
115      * 
116      * @param Tinebase_Model_Application|string $_application
117      */
118     public function initializeApplication($_application)
119     {
120         // create app root node
121         $appPath = $this->getApplicationBasePath($_application);
122         if (!$this->fileExists($appPath)) {
123             $this->mkdir($appPath);
124         }
125         
126         $sharedBasePath = $this->getApplicationBasePath($_application, Tinebase_Model_Container::TYPE_SHARED);
127         if (!$this->fileExists($sharedBasePath)) {
128             $this->mkdir($sharedBasePath);
129         }
130         
131         $personalBasePath = $this->getApplicationBasePath($_application, Tinebase_Model_Container::TYPE_PERSONAL);
132         if (!$this->fileExists($personalBasePath)) {
133             $this->mkdir($personalBasePath);
134         }
135     }
136     
137     /**
138      * get application base path
139      * 
140      * @param Tinebase_Model_Application|string $_application
141      * @param string $_type
142      * @return string
143      */
144     public function getApplicationBasePath($_application, $_type = NULL)
145     {
146         $application = $_application instanceof Tinebase_Model_Application 
147             ? $_application 
148             : Tinebase_Application::getInstance()->getApplicationById($_application);
149         
150         $result = '/' . $application->getId();
151         
152         if ($_type !== NULL) {
153             if (! in_array($_type, array(Tinebase_Model_Container::TYPE_SHARED, Tinebase_Model_Container::TYPE_PERSONAL, self::FOLDER_TYPE_RECORDS))) {
154                 throw new Tinebase_Exception_UnexpectedValue('Type can only be shared or personal.');
155             }
156             
157             $result .= '/folders/' . $_type;
158         }
159         
160         return $result;
161     } 
162     
163     /**
164      * Get one tree node (by id)
165      *
166      * @param integer|Tinebase_Record_Interface $_id
167      * @param boolean $_getDeleted get deleted records
168      * @return Tinebase_Model_Tree_Node
169      */
170     public function get($_id, $_getDeleted = FALSE)
171     {
172         $node =$this->_getTreeNodeBackend()->get($_id, $_getDeleted);
173         $fileObject = $this->_fileObjectBackend->get($node->object_id);
174         $node->description = $fileObject->description;
175         
176         return $node;
177     }
178
179     protected function _getTreeNodeBackend()
180     {
181         if ($this->_treeNodeBackend === null) {
182
183             $this->_treeNodeBackend    = new Tinebase_Tree_Node(null, /* options */ array(
184                 'modelName' => $this->_treeNodeModel,
185                 Tinebase_Config::FILESYSTEM_MODLOGACTIVE => $this->_modLogActive
186             ));
187         }
188
189         return $this->_treeNodeBackend;
190     }
191
192     /**
193      * Get multiple tree nodes identified by id
194      *
195      * @param string|array $_id Ids
196      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
197      */
198     public function getMultipleTreeNodes($_id) 
199     {
200         return$this->_getTreeNodeBackend()->getMultiple($_id);
201     }
202     
203     /**
204      * create container node
205      * 
206      * @param Tinebase_Model_Container $container
207      */
208     public function createContainerNode(Tinebase_Model_Container $container)
209     {
210         $path = $this->getContainerPath($container);
211         
212         if (!$this->fileExists($path)) {
213             $this->mkdir($path);
214         }
215     }
216
217     /**
218      * get container path
219      * 
220      * @param Tinebase_Model_Container $container
221      * @return string
222      */
223     public function getContainerPath(Tinebase_Model_Container $container)
224     {
225         $treeNodePath = new Tinebase_Model_Tree_Node_Path(array(
226             'application' => Tinebase_Application::getInstance()->getApplicationById($container->application_id)
227         ));
228         $treeNodePath->setContainer($container);
229         
230         return $treeNodePath->statpath;
231     }
232     
233     /**
234      * clear stat cache
235      * 
236      * @param string $path if given, only remove this path from statcache
237      */
238     public function clearStatCache($path = NULL)
239     {
240         if ($path !== NULL) {
241             unset($this->_statCache[$this->_getCacheId($path)]);
242         } else {
243             // clear the whole cache
244             $this->_statCache = array();
245         }
246     }
247     
248     /**
249      * copy file/directory
250      * 
251      * @todo copy recursive
252      * 
253      * @param  string  $sourcePath
254      * @param  string  $destinationPath
255      * @throws Tinebase_Exception_UnexpectedValue
256      * @return Tinebase_Model_Tree_Node
257      */
258     public function copy($sourcePath, $destinationPath)
259     {
260         $destinationNode = $this->stat($sourcePath);
261         $sourcePathParts = $this->_splitPath($sourcePath);
262         
263         try {
264             // does destinationPath exist ...
265             $parentNode = $this->stat($destinationPath);
266             
267             // ... and is a directory?
268             if (! $parentNode->type == Tinebase_Model_Tree_Node::TYPE_FOLDER) {
269                 throw new Tinebase_Exception_UnexpectedValue("Destination path exists and is a file. Please remove before.");
270             }
271             
272             $destinationNodeName  = basename(trim($sourcePath, '/'));
273             $destinationPathParts = array_merge($this->_splitPath($destinationPath), (array)$destinationNodeName);
274
275         } catch (Tinebase_Exception_NotFound $tenf) {
276             // does parent directory of destinationPath exist?
277             try {
278                 $parentNode = $this->stat(dirname($destinationPath));
279             } catch (Tinebase_Exception_NotFound $tenf) {
280                 throw new Tinebase_Exception_UnexpectedValue("Parent directory does not exist. Please create before.");
281             }
282             
283             $destinationNodeName = basename(trim($destinationPath, '/'));
284             $destinationPathParts = array_merge($this->_splitPath(dirname($destinationPath)), (array)$destinationNodeName);
285         }
286         
287         if ($sourcePathParts == $destinationPathParts) {
288             throw new Tinebase_Exception_UnexpectedValue("Source path and destination path must be different.");
289         }
290         
291         // set new node properties
292         $destinationNode->setId(null);
293         $destinationNode->parent_id = $parentNode->getId();
294         $destinationNode->name      = $destinationNodeName;
295         
296         $createdNode =$this->_getTreeNodeBackend()->create($destinationNode);
297         
298         // update hash of all parent folders
299         $this->_updateDirectoryNodesHash(dirname(implode('/', $destinationPathParts)));
300         
301         return $createdNode;
302     }
303     
304     /**
305      * get modification timestamp
306      * 
307      * @param  string  $path
308      * @return string  UNIX timestamp
309      */
310     public function getMTime($path)
311     {
312         $node = $this->stat($path);
313         
314         $timestamp = $node->last_modified_time instanceof Tinebase_DateTime 
315             ? $node->last_modified_time->getTimestamp() 
316             : $node->creation_time->getTimestamp();
317         
318         return $timestamp;
319     }
320     
321     /**
322      * check if file exists
323      * 
324      * @param  string $path
325      * @return boolean true if file/directory exists
326      */
327     public function fileExists($path) 
328     {
329         try {
330             $this->stat($path);
331         } catch (Tinebase_Exception_NotFound $tenf) {
332             return false;
333         }
334         
335         return true;
336     }
337     
338     /**
339      * close file handle
340      * 
341      * @param  resource $handle
342      * @return boolean
343      */
344     public function fclose($handle)
345     {
346         if (!is_resource($handle)) {
347             return false;
348         }
349         
350         $options = stream_context_get_options($handle);
351         
352         switch ($options['tine20']['mode']) {
353             case 'w':
354             case 'wb':
355             case 'x':
356             case 'xb':
357                 list ($hash, $hashFile) = $this->createFileBlob($handle);
358                 
359                 $this->_updateFileObject($options['tine20']['node']->object_id, $hash, $hashFile);
360                 
361                 $this->clearStatCache($options['tine20']['path']);
362                 
363                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Writing to file : ' . $options['tine20']['path'] . ' successful.');
364                 
365                 break;
366                 
367             default:
368                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Got mode : ' . $options['tine20']['mode'] . ' - nothing to do.');
369         }
370         
371         fclose($handle);
372         
373         // update hash of all parent folders
374         $this->_updateDirectoryNodesHash(dirname($options['tine20']['path']));
375         
376         return true;
377     }
378     
379     /**
380      * update file object with hash file info
381      * 
382      * @param string|Tinebase_Model_Tree_FileObject $_id file object (or id)
383      * @param string $_hash
384      * @param string $_hashFile
385      * @return Tinebase_Model_Tree_FileObject
386      */
387     protected function _updateFileObject($_id, $_hash, $_hashFile = null)
388     {
389         /** @var Tinebase_Model_Tree_FileObject $currentFileObject */
390         $currentFileObject = $_id instanceof Tinebase_Record_Abstract ? $_id : $this->_fileObjectBackend->get($_id);
391
392         if (! $_hash) {
393             // use existing hash from file object
394             $_hash = $currentFileObject->hash;
395         }
396         $_hashFile = $_hashFile ?: ($this->_basePath . '/' . substr($_hash, 0, 3) . '/' . substr($_hash, 3));
397         
398         $updatedFileObject = clone($currentFileObject);
399         $updatedFileObject->hash = $_hash;
400
401         if (file_exists($_hashFile)) {
402             $updatedFileObject->size = filesize($_hashFile);
403
404             if (function_exists('finfo_open')) {
405                 $finfo = finfo_open(FILEINFO_MIME_TYPE);
406                 $mimeType = finfo_file($finfo, $_hashFile);
407                 if ($mimeType !== false) {
408                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
409                         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Setting file contenttype to " . $mimeType);
410                     $updatedFileObject->contenttype = $mimeType;
411                 }
412                 finfo_close($finfo);
413             } else {
414                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
415                     . ' finfo_open() is not available: Could not get file information.');
416             }
417         } else {
418             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
419                 . ' File hash does not exist - directory?');
420         }
421         
422         $modLog = Tinebase_Timemachine_ModificationLog::getInstance();
423         $modLog->setRecordMetaData($updatedFileObject, 'update', $currentFileObject);
424
425         // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
426         if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
427             $updatedFileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
428             Tinebase_Server_WebDAV::getResponse()->setHeader('X-OC-MTime', 'accepted');
429             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
430                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$updatedFileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$updatedFileObject->id}");
431
432         }
433         
434         // sanitize file size, somehow filesize() seems to return empty strings on some systems
435         if (empty($updatedFileObject->size)) {
436             $updatedFileObject->size = 0;
437         }
438
439         /** @var Tinebase_Model_Tree_FileObject $newFileObject */
440         $newFileObject = $this->_fileObjectBackend->update($updatedFileObject);
441
442         $sizeDiff = ((int)$newFileObject->size) - ((int)$currentFileObject->size);
443         $revisionSizeDiff = (((int)$currentFileObject->revision) === ((int)$newFileObject->revision) ? 0 : $newFileObject->revision_size);
444
445         if ($sizeDiff !== 0 || $revisionSizeDiff > 0) {
446             // update parents with new sizes
447             $objectIds = $this->_getTreeNodeBackend()->getAllFolderNodes($this->_getTreeNodeBackend()->getObjectUsage($newFileObject->getId()))->object_id;
448             if (!empty($objectIds)) {
449                 /** @var Tinebase_Model_Tree_FileObject $fileObject */
450                 foreach($this->_fileObjectBackend->getMultiple($objectIds) as $fileObject) {
451                     if ($fileObject->getId() === $_id) {
452                         continue;
453                     }
454                     $fileObject->size = ((int)$fileObject->size) + $sizeDiff;
455                     $fileObject->revision_size = ((int)$fileObject->revision_size) + $revisionSizeDiff;
456                     $this->_fileObjectBackend->update($fileObject);
457                 }
458             }
459         }
460
461         if (true === Tinebase_Config::getInstance()->get(Tinebase_Config::FILESYSTEM)->{Tinebase_Config::FILESYSTEM_INDEX_CONTENT}) {
462             Tinebase_ActionQueue::getInstance()->queueAction('Tinebase_FOO_FileSystem.indexFileObject', $newFileObject->getId());
463         }
464
465         return $newFileObject;
466     }
467
468     /**
469      * @param string $_objectId
470      * @return bool
471      */
472     public function indexFileObject($_objectId)
473     {
474         /** @var Tinebase_Model_Tree_FileObject $fileObject */
475         try {
476             $fileObject = $this->_fileObjectBackend->get($_objectId);
477         } catch(Tinebase_Exception_NotFound $tenf) {
478             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
479                 . ' Could not find file object ' . $_objectId);
480             return true;
481         }
482         if (Tinebase_Model_Tree_FileObject::TYPE_FILE !== $fileObject->type) {
483             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
484                 . ' file object ' . $_objectId . ' is not a file: ' . $fileObject->type);
485             return true;
486         }
487         if ($fileObject->hash === $fileObject->indexed_hash) {
488             // nothing to do
489             return true;
490         }
491
492         // we clean up $tmpFile down there in finally
493         if (false === ($tmpFile = Tinebase_Fulltext_TextExtract::getInstance()->fileObjectToTempFile($fileObject))) {
494             return false;
495         }
496
497         $indexedHash = $fileObject->hash;
498
499         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
500
501         try {
502
503             try {
504                 $fileObject = $this->_fileObjectBackend->get($_objectId);
505             } catch(Tinebase_Exception_NotFound $tenf) {
506                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
507                     . ' Could not find file object ' . $_objectId);
508                 return true;
509             }
510             if (Tinebase_Model_Tree_FileObject::TYPE_FILE !== $fileObject->type) {
511                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
512                     . ' file object ' . $_objectId . ' is not a file: ' . $fileObject->type);
513                 return true;
514             }
515             if ($fileObject->hash === $fileObject->indexed_hash || $indexedHash === $fileObject->indexed_hash) {
516                 // nothing to do
517                 return true;
518             }
519
520             Tinebase_Fulltext_Indexer::getInstance()->addFileContentsToIndex($fileObject->getId(), $tmpFile);
521
522             $fileObject->indexed_hash = $indexedHash;
523             $this->_fileObjectBackend->update($fileObject);
524
525             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
526
527         } catch (Exception $e) {
528             Tinebase_Exception::log($e);
529             Tinebase_TransactionManager::getInstance()->rollBack();
530
531             return false;
532
533         } finally {
534             unlink($tmpFile);
535         }
536
537         return true;
538     }
539     
540     /**
541      * update hash of all directories for given path
542      * 
543      * @param string $path
544      */
545     protected function _updateDirectoryNodesHash($path)
546     {
547         // update hash of all parent folders
548         $parentNodes = $this->_getPathNodes($path);
549         $updatedNodes = $this->_fileObjectBackend->updateDirectoryNodesHash($parentNodes);
550         
551         // update nodes stored in local statCache
552         $subPath = null;
553         /** @var Tinebase_Model_Tree_Node $node */
554         foreach ($parentNodes as $node) {
555             /** @var Tinebase_Model_Tree_FileObject $directoryObject */
556             $directoryObject = $updatedNodes->getById($node->object_id);
557             
558             if ($directoryObject) {
559                 $node->revision             = $directoryObject->revision;
560                 $node->hash                 = $directoryObject->hash;
561                 $node->size                 = $directoryObject->size;
562                 $node->revision_size        = $directoryObject->revision_size;
563                 $node->available_revisions  = $directoryObject->available_revisions;
564             }
565             
566             $subPath .= "/" . $node->name;
567             $this->_addStatCache($subPath, $node);
568         }
569     }
570     
571     /**
572      * open file
573      * 
574      * @param string $_path
575      * @param string $_mode
576      * @param int|null $_revision
577      * @return resource|boolean
578      */
579     public function fopen($_path, $_mode, $_revision = null)
580     {
581         $dirName = dirname($_path);
582         $fileName = basename($_path);
583         
584         switch ($_mode) {
585             // Create and open for writing only; place the file pointer at the beginning of the file. 
586             // If the file already exists, the fopen() call will fail by returning FALSE and generating 
587             // an error of level E_WARNING. If the file does not exist, attempt to create it. This is 
588             // equivalent to specifying O_EXCL|O_CREAT flags for the underlying open(2) system call.
589             case 'x':
590             case 'xb':
591                 if (!$this->isDir($dirName) || $this->fileExists($_path)) {
592                     return false;
593                 }
594                 
595                 $parent = $this->stat($dirName);
596                 $node = $this->createFileTreeNode($parent, $fileName);
597                 
598                 $handle = Tinebase_TempFile::getInstance()->openTempFile();
599                 
600                 break;
601                 
602             // Open for reading only; place the file pointer at the beginning of the file.
603             case 'r':
604             case 'rb':
605                 if ($this->isDir($_path) || !$this->fileExists($_path)) {
606                     return false;
607                 }
608                 
609                 $node = $this->stat($_path, $_revision);
610                 $hashFile = $this->_basePath . '/' . substr($node->hash, 0, 3) . '/' . substr($node->hash, 3);
611                 
612                 $handle = fopen($hashFile, $_mode);
613                 
614                 break;
615                 
616             // Open for writing only; place the file pointer at the beginning of the file and truncate the 
617             // file to zero length. If the file does not exist, attempt to create it.
618             case 'w':
619             case 'wb':
620                 if (!$this->isDir($dirName)) {
621                     return false;
622                 }
623                 
624                 if (!$this->fileExists($_path)) {
625                     $parent = $this->stat($dirName);
626                     $node = $this->createFileTreeNode($parent, $fileName);
627                 } else {
628                     $node = $this->stat($_path, $_revision);
629                 }
630                 
631                 $handle = Tinebase_TempFile::getInstance()->openTempFile();
632                 
633                 break;
634                 
635             default:
636                 return false;
637         }
638         
639         $contextOptions = array('tine20' => array(
640             'path' => $_path,
641             'mode' => $_mode,
642             'node' => $node
643         ));
644         stream_context_set_option($handle, $contextOptions);
645         
646         return $handle;
647     }
648     
649     /**
650      * get content type
651      * 
652      * @deprecated use Tinebase_FileSystem::stat()->contenttype
653      * @param  string  $path
654      * @return string
655      */
656     public function getContentType($path)
657     {
658         $node = $this->stat($path);
659         
660         return $node->contenttype;
661     }
662     
663     /**
664      * get etag
665      * 
666      * @deprecated use Tinebase_FileSystem::stat()->hash
667      * @param  string $path
668      * @return string
669      */
670     public function getETag($path)
671     {
672         $node = $this->stat($path);
673         
674         return $node->hash;
675     }
676     
677     /**
678      * return if path is a directory
679      * 
680      * @param  string  $path
681      * @return boolean
682      */
683     public function isDir($path)
684     {
685         try {
686             $node = $this->stat($path);
687         } catch (Tinebase_Exception_InvalidArgument $teia) {
688             return false;
689         } catch (Tinebase_Exception_NotFound $tenf) {
690             return false;
691         }
692         
693         if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
694             return false;
695         }
696         
697         return true;
698     }
699     
700     /**
701      * return if path is a file
702      *
703      * @param  string  $path
704      * @return boolean
705      */
706     public function isFile($path)
707     {
708         try {
709             $node = $this->stat($path);
710         } catch (Tinebase_Exception_InvalidArgument $teia) {
711             return false;
712         } catch (Tinebase_Exception_NotFound $tenf) {
713             return false;
714         }
715     
716         if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FILE) {
717             return false;
718         }
719     
720         return true;
721     }
722     
723     /**
724      * rename file/directory
725      *
726      * @param  string  $oldPath
727      * @param  string  $newPath
728      * @return Tinebase_Model_Tree_Node|boolean
729      */
730     public function rename($oldPath, $newPath)
731     {
732         try {
733             $node = $this->stat($oldPath);
734         } catch (Tinebase_Exception_InvalidArgument $teia) {
735             return false;
736         } catch (Tinebase_Exception_NotFound $tenf) {
737             return false;
738         }
739     
740         if (dirname($oldPath) != dirname($newPath)) {
741             try {
742                 $newParent = $this->stat(dirname($newPath));
743             } catch (Tinebase_Exception_InvalidArgument $teia) {
744                 return false;
745             } catch (Tinebase_Exception_NotFound $tenf) {
746                 return false;
747             }
748     
749             $node->parent_id = $newParent->getId();
750         }
751     
752         if (basename($oldPath) != basename($newPath)) {
753             $node->name = basename($newPath);
754         }
755     
756         $node =$this->_getTreeNodeBackend()->update($node);
757         
758         $this->clearStatCache($oldPath);
759         
760         $this->_addStatCache($newPath, $node);
761         
762         return $node;
763     }
764     
765     /**
766      * create directory
767      * 
768      * @param string $path
769      */
770     public function mkdir($path)
771     {
772         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
773             . ' Creating directory ' . $path);
774         
775         $currentPath = array();
776         $parentNode  = null;
777         $pathParts   = $this->_splitPath($path);
778         $node = null;
779
780         foreach ($pathParts as $pathPart) {
781             $pathPart = trim($pathPart);
782             $currentPath[]= $pathPart;
783             
784             try {
785                 $node = $this->stat('/' . implode('/', $currentPath));
786             } catch (Tinebase_Exception_NotFound $tenf) {
787                 $node = $this->createDirectoryTreeNode($parentNode, $pathPart);
788                 
789                 $this->_addStatCache($currentPath, $node);
790             }
791             
792             $parentNode = $node;
793         }
794         
795         // update hash of all parent folders
796         $this->_updateDirectoryNodesHash($path);
797         
798         return $node;
799     }
800     
801     /**
802      * remove directory
803      * 
804      * @param  string   $path
805      * @param  boolean  $recursive
806      * @return boolean
807      */
808     public function rmdir($path, $recursive = FALSE)
809     {
810         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
811             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Removing directory ' . $path);
812         
813         $node = $this->stat($path);
814         
815         $children = $this->getTreeNodeChildren($node);
816         
817         // check if child entries exists and delete if $_recursive is true
818         if (count($children) > 0) {
819             if ($recursive !== true) {
820                 throw new Tinebase_Exception_InvalidArgument('directory not empty');
821             } else {
822                 foreach ($children as $child) {
823                     if ($this->isDir($path . '/' . $child->name)) {
824                         $this->rmdir($path . '/' . $child->name, true);
825                     } else {
826                         $this->unlink($path . '/' . $child->name);
827                     }
828                 }
829             }
830         }
831         
832         $this->_getTreeNodeBackend()->delete($node->getId());
833         $this->clearStatCache($path);
834
835         // delete object only, if no other tree node refers to it
836         // we can use treeNodeBackend property because getTreeNodeBackend was called just above
837         if ($this->_treeNodeBackend->getObjectCount($node->object_id) == 0) {
838             $this->_fileObjectBackend->softDelete($node->object_id);
839             if (false === $this->_modLogActive && true === $this->_indexingActive) {
840                 Tinebase_Fulltext_Indexer::getInstance()->removeFileContentsFromIndex($node->object_id);
841             }
842         }
843         
844         return true;
845     }
846     
847     /**
848      * scan dir
849      * 
850      * @param  string  $path
851      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
852      */
853     public function scanDir($path)
854     {
855         $children = $this->getTreeNodeChildren($this->stat($path));
856         
857         foreach ($children as $node) {
858             $this->_addStatCache($path . '/' . $node->name, $node);
859         }
860         
861         return $children;
862     }
863     
864     /**
865      * @param  string  $path
866      * @param  int|null $revision
867      * @return Tinebase_Model_Tree_Node
868      * @throws Tinebase_Exception_NotFound
869      */
870     public function stat($path, $revision = null)
871     {
872         $pathParts = $this->_splitPath($path);
873         $cacheId = $this->_getCacheId($pathParts, $revision);
874         
875         // let's see if the path is cached in statCache
876         if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
877             try {
878                 // let's try to get the node from backend, to make sure it still exists
879                 $this->_getTreeNodeBackend()->setRevision($revision);
880                 return $this->_checkRevision($this->_getTreeNodeBackend()->get($this->_statCache[$cacheId]), $revision);
881             } catch (Tinebase_Exception_NotFound $tenf) {
882                 // something went wrong. let's clear the whole statCache
883                 $this->clearStatCache();
884             } finally {
885                 $this->_getTreeNodeBackend()->setRevision(null);
886             }
887         }
888         
889         $parentNode = null;
890         $node       = null;
891         
892         // find out if we have cached any node up in the path
893         do {
894             $cacheId = $this->_getCacheId($pathParts);
895             
896             if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
897                 $node = $parentNode = $this->_statCache[$cacheId];
898                 break;
899             }
900         } while (($pathPart = array_pop($pathParts) !== null));
901         
902         $missingPathParts = array_diff_assoc($this->_splitPath($path), $pathParts);
903         
904         foreach ($missingPathParts as $pathPart) {
905             $node = $this->_getTreeNodeBackend()->getChild($parentNode, $pathPart);
906             
907             // keep track of current path position
908             array_push($pathParts, $pathPart);
909             
910             // add found path to statCache
911             $this->_addStatCache($pathParts, $node);
912             
913             $parentNode = $node;
914         }
915
916         if (null !== $revision) {
917             try {
918                 $this->_getTreeNodeBackend()->setRevision($revision);
919                 $node = $this->_checkRevision($this->_getTreeNodeBackend()->get($node->getId()), $revision);
920
921                 // add found path to statCache
922                 $this->_addStatCache($pathParts, $node, $revision);
923             } finally {
924                 $this->_getTreeNodeBackend()->setRevision(null);
925             }
926         }
927
928         return $node;
929     }
930
931     /**
932      * @param Tinebase_Model_Tree_Node $_node
933      * @param int|null $_revision
934      * @return Tinebase_Model_Tree_Node
935      * @throws Tinebase_Exception_NotFound
936      */
937     protected function _checkRevision(Tinebase_Model_Tree_Node $_node, $_revision)
938     {
939         if (null !== $_revision && empty($_node->hash)) {
940             throw new Tinebase_Exception_NotFound('file does not have revision: ' . $_revision);
941         }
942
943         return $_node;
944     }
945
946     /**
947      * get filesize
948      * 
949      * @deprecated use Tinebase_FileSystem::stat()->size
950      * @param  string  $path
951      * @return integer
952      */
953     public function filesize($path)
954     {
955         $node = $this->stat($path);
956         
957         return $node->size;
958     }
959     
960     /**
961      * delete file
962      * 
963      * @param  string  $_path
964      * @return boolean
965      */
966     public function unlink($path)
967     {
968         $node = $this->stat($path);
969         $this->deleteFileNode($node);
970         
971         $this->clearStatCache($path);
972         
973         // update hash of all parent folders
974         $this->_updateDirectoryNodesHash(dirname($path));
975         
976         return true;
977     }
978     
979     /**
980      * delete file node
981      * 
982      * @param Tinebase_Model_Tree_Node $node
983      */
984     public function deleteFileNode(Tinebase_Model_Tree_Node $node)
985     {
986         if ($node->type == Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
987             throw new Tinebase_Exception_InvalidArgument('can not unlink directories');
988         }
989         
990        $this->_getTreeNodeBackend()->delete($node->getId());
991         
992         // delete object only, if no one uses it anymore
993         // we can use treeNodeBackend property because getTreeNodeBackend was called just above
994         if ($this->_treeNodeBackend->getObjectCount($node->object_id) == 0) {
995             $this->_fileObjectBackend->softDelete($node->object_id);
996             if (false === $this->_modLogActive && true === $this->_indexingActive) {
997                 Tinebase_Fulltext_Indexer::getInstance()->removeFileContentsFromIndex($node->object_id);
998             }
999         }
1000     }
1001     
1002     /**
1003      * create directory
1004      * 
1005      * @param  string|Tinebase_Model_Tree_Node  $parentId
1006      * @param  string                           $name
1007      * @return Tinebase_Model_Tree_Node
1008      */
1009     public function createDirectoryTreeNode($parentId, $name)
1010     {
1011         $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
1012         
1013         $directoryObject = new Tinebase_Model_Tree_FileObject(array(
1014             'type'          => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
1015             'contentytype'  => null,
1016             'hash'          => Tinebase_Record_Abstract::generateUID(),
1017             'size'          => 0
1018         ));
1019         Tinebase_Timemachine_ModificationLog::setRecordMetaData($directoryObject, 'create');
1020         $directoryObject = $this->_fileObjectBackend->create($directoryObject);
1021         
1022         $treeNode = new Tinebase_Model_Tree_Node(array(
1023             'name'          => $name,
1024             'object_id'     => $directoryObject->getId(),
1025             'parent_id'     => $parentId
1026         ));
1027         $treeNode =$this->_getTreeNodeBackend()->create($treeNode);
1028         
1029         return $treeNode;
1030     }
1031     
1032     /**
1033      * create new file node
1034      * 
1035      * @param  string|Tinebase_Model_Tree_Node  $parentId
1036      * @param  string                           $name
1037      * @throws Tinebase_Exception_InvalidArgument
1038      * @return Tinebase_Model_Tree_Node
1039      */
1040     public function createFileTreeNode($parentId, $name)
1041     {
1042         $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
1043         
1044         $fileObject = new Tinebase_Model_Tree_FileObject(array(
1045             'type'          => Tinebase_Model_Tree_FileObject::TYPE_FILE,
1046             'contentytype'  => null,
1047         ));
1048         Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'create');
1049
1050         // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
1051         if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
1052             $fileObject->creation_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1053             $fileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1054             Tinebase_Server_WebDAV::getResponse()->setHeader('X-OC-MTime', 'accepted');
1055             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
1056                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$fileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$name}");
1057
1058         }
1059
1060         $fileObject = $this->_fileObjectBackend->create($fileObject);
1061
1062         $treeNode = new Tinebase_Model_Tree_Node(array(
1063             'name'          => $name,
1064             'object_id'     => $fileObject->getId(),
1065             'parent_id'     => $parentId
1066         ));
1067         
1068         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1069             ' ' . print_r($treeNode->toArray(), TRUE));
1070         
1071         $treeNode =$this->_getTreeNodeBackend()->create($treeNode);
1072
1073         return $treeNode;
1074     }
1075
1076     /**
1077      * places contents into a file blob
1078      * 
1079      * @param  resource $contents
1080      * @return string hash
1081      * @throws Tinebase_Exception_NotImplemented
1082      */
1083     public function createFileBlob($contents)
1084     {
1085         if (! is_resource($contents)) {
1086             throw new Tinebase_Exception_NotImplemented('please implement me!');
1087         }
1088         
1089         $handle = $contents;
1090         rewind($handle);
1091         
1092         $ctx = hash_init('sha1');
1093         hash_update_stream($ctx, $handle);
1094         $hash = hash_final($ctx);
1095         
1096         $hashDirectory = $this->_basePath . '/' . substr($hash, 0, 3);
1097         if (!file_exists($hashDirectory)) {
1098             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash directory: ' . $hashDirectory);
1099             if(mkdir($hashDirectory, 0700) === false) {
1100                 throw new Tinebase_Exception_UnexpectedValue('failed to create directory');
1101             }
1102         }
1103         
1104         $hashFile      = $hashDirectory . '/' . substr($hash, 3);
1105         if (!file_exists($hashFile)) {
1106             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash file: ' . $hashFile);
1107             rewind($handle);
1108             $hashHandle = fopen($hashFile, 'x');
1109             stream_copy_to_stream($handle, $hashHandle);
1110             fclose($hashHandle);
1111         }
1112         
1113         return array($hash, $hashFile);
1114     }
1115     
1116     /**
1117      * get tree node children
1118      * 
1119      * @param string|Tinebase_Model_Tree_Node|Tinebase_Record_RecordSet  $nodeId
1120      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
1121      */
1122     public function getTreeNodeChildren($nodeId)
1123     {
1124         if ($nodeId instanceof Tinebase_Model_Tree_Node) {
1125             $nodeId = $nodeId->getId();
1126             $operator = 'equals';
1127         } elseif ($nodeId instanceof Tinebase_Record_RecordSet) {
1128             $nodeId = $nodeId->getArrayOfIds();
1129             $operator = 'in';
1130         } else {
1131             $operator = 'equals';
1132         }
1133         
1134         $searchFilter = new Tinebase_Model_Tree_Node_Filter(array(
1135             array(
1136                 'field'     => 'parent_id',
1137                 'operator'  => $operator,
1138                 'value'     => $nodeId
1139             )
1140         ));
1141         $children = $this->searchNodes($searchFilter);
1142         
1143         return $children;
1144     }
1145     
1146     /**
1147      * search tree nodes
1148      * 
1149      * @param Tinebase_Model_Tree_Node_Filter $_filter
1150      * @param Tinebase_Record_Interface $_pagination
1151      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
1152      */
1153     public function searchNodes(Tinebase_Model_Tree_Node_Filter $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL)
1154     {
1155         $result =$this->_getTreeNodeBackend()->search($_filter, $_pagination);
1156         return $result;
1157     }
1158
1159     /**
1160      * search tree nodes
1161      *
1162      * TODO replace searchNodes / or refactor this - tree objects has no search function yet / might be ambiguous...
1163      *
1164      * @param Tinebase_Model_Tree_Node_Filter $_filter
1165      * @param Tinebase_Record_Interface $_pagination
1166      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
1167      */
1168     public function search(Tinebase_Model_Tree_Node_Filter $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $onlyIds = false)
1169     {
1170         $result = $this->_getTreeNodeBackend()->search($_filter, $_pagination, $onlyIds);
1171         return $result;
1172     }
1173
1174     /**
1175     * search tree nodes count
1176     *
1177     * @param Tinebase_Model_Tree_Node_Filter $_filter
1178     * @return integer
1179     */
1180     public function searchNodesCount(Tinebase_Model_Tree_Node_Filter $_filter = NULL)
1181     {
1182         $result =$this->_getTreeNodeBackend()->searchCount($_filter);
1183         return $result;
1184     }
1185     
1186     /**
1187      * get nodes by container (or container id)
1188      * 
1189      * @param int|Tinebase_Model_Container $container
1190      * @return Tinebase_Record_RecordSet
1191      */
1192     public function getNodesByContainer($container)
1193     {
1194         $nodeContainer = ($container instanceof Tinebase_Model_Container) ? $container : Tinebase_Container::getInstance()->getContainerById($container);
1195         $path = $this->getContainerPath($nodeContainer);
1196         $parentNode = $this->stat($path);
1197         $filter = new Tinebase_Model_Tree_Node_Filter(array(
1198             array('field' => 'parent_id', 'operator' => 'equals', 'value' => $parentNode->getId())
1199         ));
1200         
1201         return $this->searchNodes($filter);
1202     }
1203     
1204     /**
1205      * get tree node specified by parent node (or id) and name
1206      * 
1207      * @param string|Tinebase_Model_Tree_Node $_parentId
1208      * @param string $_name
1209      * @throws Tinebase_Exception_InvalidArgument
1210      * @return Tinebase_Model_Tree_Node
1211      */
1212     public function getTreeNode($_parentId, $_name)
1213     {
1214         $parentId = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId->getId() : $_parentId;
1215         
1216         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1217             . ' Getting tree node ' . $parentId . '/'. $_name);
1218         
1219         return$this->_getTreeNodeBackend()->getChild($_parentId, $_name);
1220     }
1221     
1222     /**
1223      * add entry to stat cache
1224      * 
1225      * @param string|array              $path
1226      * @param Tinebase_Model_Tree_Node  $node
1227      * @param int|null                  $revision
1228      */
1229     protected function _addStatCache($path, Tinebase_Model_Tree_Node $node, $revision = null)
1230     {
1231         $this->_statCache[$this->_getCacheId($path, $revision)] = $node;
1232     }
1233     
1234     /**
1235      * generate cache id
1236      * 
1237      * @param  string|array  $path
1238      * @param  int|null $revision
1239      * @return string
1240      */
1241     protected function _getCacheId($path, $revision = null)
1242     {
1243         $pathParts = is_array($path) ? $path : $this->_splitPath($path);
1244         array_unshift($pathParts, '@' . $revision);
1245
1246         return sha1(implode(null, $pathParts));
1247     }
1248     
1249     /**
1250      * split path
1251      * 
1252      * @param  string  $path
1253      * @return array
1254      */
1255     protected function _splitPath($path)
1256     {
1257         return explode('/', trim($path, '/'));
1258     }
1259     
1260     /**
1261      * update node
1262      * 
1263      * @param Tinebase_Model_Tree_Node $_node
1264      * @return Tinebase_Model_Tree_Node
1265      */
1266     public function update(Tinebase_Model_Tree_Node $_node)
1267     {
1268         $currentNodeObject = $this->get($_node->getId());
1269         $fileObject = $this->_fileObjectBackend->get($currentNodeObject->object_id);
1270
1271         Tinebase_Timemachine_ModificationLog::setRecordMetaData($_node, 'update', $currentNodeObject);
1272         Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'update', $fileObject);
1273
1274         // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
1275         if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
1276             $fileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1277             Tinebase_Server_WebDAV::getResponse()->setHeader('X-OC-MTime', 'accepted');
1278             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
1279                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$fileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$_node->name}");
1280
1281         }
1282
1283         // update file object
1284         $fileObject->description = $_node->description;
1285         $this->_updateFileObject($fileObject, $_node->hash);
1286         
1287         return $this->_getTreeNodeBackend()->update($_node);
1288     }
1289     
1290     /**
1291      * get container of node
1292      * 
1293      * @param Tinebase_Model_Tree_Node|string $node
1294      * @return Tinebase_Model_Container
1295      */
1296     public function getNodeContainer($node)
1297     {
1298         $nodesPath = $this->getPathOfNode($node);
1299         
1300         if (count($nodesPath) < 4) {
1301             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
1302                 ' ' . print_r($nodesPath[0], TRUE));
1303             throw new Tinebase_Exception_NotFound('Could not find container for node ' . $nodesPath[0]['id']);
1304         }
1305         
1306         $containerNode = ($nodesPath[2]['name'] === Tinebase_Model_Container::TYPE_PERSONAL) ? $nodesPath[4] : $nodesPath[3];
1307         return Tinebase_Container::getInstance()->get($containerNode['name']);
1308     }
1309     
1310     /**
1311      * get path of node
1312      * 
1313      * @param Tinebase_Model_Tree_Node|string $node
1314      * @param boolean $getPathAsString
1315      * @return array|string
1316      */
1317     public function getPathOfNode($node, $getPathAsString = FALSE)
1318     {
1319         $node = $node instanceof Tinebase_Model_Tree_Node ? $node : $this->get($node);
1320         
1321         $nodesPath = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($node));
1322         while ($node->parent_id) {
1323             $node = $this->get($node->parent_id);
1324             $nodesPath->addRecord($node);
1325         }
1326         
1327         $result = ($getPathAsString) ? '/' . implode('/', array_reverse($nodesPath->name)) : array_reverse($nodesPath->toArray());
1328         return $result;
1329     }
1330     
1331     protected function _getPathNodes($path)
1332     {
1333         $pathParts = $this->_splitPath($path);
1334         
1335         if (empty($pathParts)) {
1336             throw new Tinebase_Exception_InvalidArgument('empty path provided');
1337         }
1338         
1339         $subPath   = null;
1340         $pathNodes = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
1341         
1342         foreach ($pathParts as $pathPart) {
1343             $subPath .= "/$pathPart"; 
1344             
1345             $node = $this->stat($subPath);
1346             if ($node) {
1347                 $pathNodes->addRecord($node);
1348             }
1349         }
1350         
1351         return $pathNodes;
1352     }
1353     
1354     /**
1355      * clears deleted files from filesystem + database
1356      */
1357     public function clearDeletedFiles()
1358     {
1359         $this->clearDeletedFilesFromFilesystem();
1360         $this->clearDeletedFilesFromDatabase();
1361     }
1362     
1363     /**
1364      * removes deleted files that no longer exist in the database from the filesystem
1365      * 
1366      * @return integer number of deleted files
1367      */
1368     public function clearDeletedFilesFromFilesystem()
1369     {
1370         try {
1371             $dirIterator = new DirectoryIterator($this->_basePath);
1372         } catch (Exception $e) {
1373             throw new Tinebase_Exception_AccessDenied('Could not open files directory.');
1374         }
1375         
1376         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1377             . ' Scanning ' . $this->_basePath . ' for deleted files ...');
1378         
1379         $deleteCount = 0;
1380         /** @var DirectoryIterator $item */
1381         foreach ($dirIterator as $item) {
1382             if (!$item->isDir()) {
1383                 continue;
1384             }
1385             $subDir = $item->getFilename();
1386             if ($subDir[0] == '.') continue;
1387             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1388                 . ' Checking ' . $subDir);
1389             $subDirIterator = new DirectoryIterator($this->_basePath . '/' . $subDir);
1390             $hashsToCheck = array();
1391             // loop dirs + check if files in dir are in tree_filerevisions
1392             foreach ($subDirIterator as $file) {
1393                 if ($file->isFile()) {
1394                     $hash = $subDir . $file->getFilename();
1395                     $hashsToCheck[] = $hash;
1396                 }
1397             }
1398             $existingHashes = $this->_fileObjectBackend->checkRevisions($hashsToCheck);
1399             $hashesToDelete = array_diff($hashsToCheck, $existingHashes);
1400             // remove from filesystem if not existing any more
1401             foreach ($hashesToDelete as $hashToDelete) {
1402                 $filename = $this->_basePath . '/' . $subDir . '/' . substr($hashToDelete, 3);
1403                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1404                     . ' Deleting ' . $filename);
1405                 unlink($filename);
1406                 $deleteCount++;
1407             }
1408         }
1409         
1410         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1411             . ' Deleted ' . $deleteCount . ' obsolete file(s).');
1412         
1413         return $deleteCount;
1414     }
1415     
1416     /**
1417      * removes deleted files that no longer exist in the filesystem from the database
1418      * 
1419      * @return integer number of deleted files
1420      */
1421     public function clearDeletedFilesFromDatabase()
1422     {
1423         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1424             . ' Scanning database for deleted files ...');
1425
1426         // get all file objects from db and check filesystem existance
1427         $filter = new Tinebase_Model_Tree_FileObjectFilter();
1428         $start = 0;
1429         $limit = 500;
1430         $toDeleteIds = array();
1431
1432         do {
1433             $pagination = new Tinebase_Model_Pagination(array(
1434                 'start' => $start,
1435                 'limit' => $limit,
1436                 'sort' => 'id',
1437             ));
1438
1439             $fileObjects = $this->_fileObjectBackend->search($filter, $pagination);
1440             foreach ($fileObjects as $fileObject) {
1441                 if ($fileObject->type === Tinebase_Model_Tree_FileObject::TYPE_FILE && $fileObject->hash && !file_exists($fileObject->getFilesystemPath())) {
1442                     $toDeleteIds[] = $fileObject->getId();
1443                 }
1444             }
1445
1446             $start += $limit;
1447         } while ($fileObjects->count() >= $limit);
1448
1449         if (count($toDeleteIds) === 0) {
1450             return 0;
1451         }
1452
1453         $nodeIdsToDelete =$this->_getTreeNodeBackend()->search(new Tinebase_Model_Tree_Node_Filter(array(array(
1454             'field'     => 'object_id',
1455             'operator'  => 'in',
1456             'value'     => $toDeleteIds
1457         ))), NULL, Tinebase_Backend_Sql_Abstract::IDCOL);
1458
1459         // hard delete is ok here
1460         $deleteCount = $this->_getTreeNodeBackend()->delete($nodeIdsToDelete);
1461         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1462             . ' Removed ' . $deleteCount . ' obsolete filenode(s) from the database.');
1463
1464         $this->_fileObjectBackend->softDelete($toDeleteIds);
1465         if (false === $this->_modLogActive && true === $this->_indexingActive) {
1466             Tinebase_Fulltext_Indexer::getInstance()->removeFileContentsFromIndex($toDeleteIds);
1467         }
1468
1469         return $deleteCount;
1470     }
1471
1472     /**
1473      * copy tempfile data to file path
1474      * 
1475      * @param  mixed   $tempFile
1476          Tinebase_Model_Tree_Node     with property hash, tempfile or stream
1477          Tinebase_Model_Tempfile      tempfile
1478          string                       with tempFile id
1479          array                        with [id] => tempFile id (this is odd IMHO)
1480          stream                       stream ressource
1481          NULL                         create empty file
1482      * @param  string  $path
1483      * @throws Tinebase_Exception_AccessDenied
1484      */
1485     public function copyTempfile($tempFile, $path)
1486     {
1487         if ($tempFile === NULL) {
1488             $tempStream = fopen('php://memory', 'r');
1489         } else if (is_resource($tempFile)) {
1490             $tempStream = $tempFile;
1491         } else if (is_string($tempFile) || is_array($tempFile)) {
1492             $tempFile = Tinebase_TempFile::getInstance()->getTempFile($tempFile);
1493             $this->copyTempfile($tempFile, $path);
1494             return;
1495         } else if ($tempFile instanceof Tinebase_Model_Tree_Node) {
1496             if (isset($tempFile->hash)) {
1497                 $hashFile = $this->_basePath . '/' . substr($tempFile->hash, 0, 3) . '/' . substr($tempFile->hash, 3);
1498                 $tempStream = fopen($hashFile, 'r');
1499             } else if (is_resource($tempFile->stream)) {
1500                 $tempStream = $tempFile->stream;
1501             } else {
1502                 $this->copyTempfile($tempFile->tempFile, $path);
1503                 return;
1504             }
1505         } else if ($tempFile instanceof Tinebase_Model_TempFile) {
1506             $tempStream = fopen($tempFile->path, 'r');
1507         } else {
1508             throw new Tinebase_Exception_UnexpectedValue('unexpected tempfile value');
1509         }
1510         
1511         $this->copyStream($tempStream, $path);
1512     }
1513     
1514     /**
1515      * copy stream data to file path
1516      *
1517      * @param  resource  $in
1518      * @param  string  $path
1519      * @throws Tinebase_Exception_AccessDenied
1520      * @throws Tinebase_Exception_UnexpectedValue
1521      */
1522     public function copyStream($in, $path)
1523     {
1524         if (! $handle = $this->fopen($path, 'w')) {
1525             throw new Tinebase_Exception_AccessDenied('Permission denied to create file (filename ' . $path . ')');
1526         }
1527         
1528         if (! is_resource($in)) {
1529             throw new Tinebase_Exception_UnexpectedValue('source needs to be of type stream');
1530         }
1531         
1532         if (is_resource($in) !== NULL) {
1533             $metaData = stream_get_meta_data($in);
1534             if (true === $metaData['seekable']) {
1535                 rewind($in);
1536             }
1537             stream_copy_to_stream($in, $handle);
1538             
1539             $this->clearStatCache($path);
1540         }
1541         
1542         $this->fclose($handle);
1543     }
1544
1545     /**
1546      * recalculates all revision sizes of file objects of type file only
1547      *
1548      * on error it still continues and tries to calculate as many revision sizes as possible, but returns false
1549      *
1550      * @return bool
1551      */
1552     public function recalculateRevisionSize()
1553     {
1554         return $this->_fileObjectBackend->recalculateRevisionSize();
1555     }
1556
1557     /**
1558      * recalculates all folder sizes
1559      *
1560      * on error it still continues and tries to calculate as many folder sizes as possible, but returns false
1561      *
1562      * @return bool
1563      */
1564     public function recalculateFolderSize()
1565     {
1566         return $this->_getTreeNodeBackend()->recalculateFolderSize($this->_fileObjectBackend);
1567     }
1568
1569     /**
1570      * indexes all not indexed file objects
1571      *
1572      * on error it still continues and tries to index as many file objects as possible, but returns false
1573      *
1574      * @return bool
1575      */
1576     public function checkIndexing()
1577     {
1578         if (false === $this->_indexingActive) {
1579             return true;
1580         }
1581
1582         $success = true;
1583         foreach($this->_fileObjectBackend->getNotIndexedObjectIds() as $objectId) {
1584             $success = $this->indexFileObject($objectId) && $success;
1585         }
1586
1587         return $success;
1588     }
1589 }