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)
11 * @todo 0007376: Tinebase_FileSystem / Node model refactoring: move all container related functionality to Filemanager
15 * filesystem controller
18 * @subpackage FileSystem
20 class Tinebase_FileSystem implements Tinebase_Controller_Interface
23 * folder name/type for record attachments
27 const FOLDER_TYPE_RECORDS = 'records';
30 * @var Tinebase_Tree_FileObject
32 protected $_fileObjectBackend;
35 * @var Tinebase_Tree_Node
37 protected $_treeNodeBackend;
40 * path where physical files gets stored
51 protected $_statCache = array();
54 * holds the instance of the singleton
56 * @var Tinebase_FileSystem
58 private static $_instance = NULL;
63 public function __construct()
65 $this->_fileObjectBackend = new Tinebase_Tree_FileObject();
66 $this->_treeNodeBackend = new Tinebase_Tree_Node();
68 if (! Setup_Controller::getInstance()->isFilesystemAvailable()) {
69 throw new Tinebase_Exception_Backend('No base path (filesdir) configured or path not writeable');
72 $this->_basePath = Tinebase_Core::getConfig()->filesdir;
76 * the singleton pattern
78 * @return Tinebase_FileSystem
80 public static function getInstance()
82 if (self::$_instance === NULL) {
83 self::$_instance = new Tinebase_FileSystem;
86 return self::$_instance;
90 * init application base paths
92 * @param Tinebase_Model_Application|string $_application
94 public function initializeApplication($_application)
96 // create app root node
97 $appPath = $this->getApplicationBasePath($_application);
98 if (!$this->fileExists($appPath)) {
99 $this->mkdir($appPath);
102 $sharedBasePath = $this->getApplicationBasePath($_application, Tinebase_Model_Container::TYPE_SHARED);
103 if (!$this->fileExists($sharedBasePath)) {
104 $this->mkdir($sharedBasePath);
107 $personalBasePath = $this->getApplicationBasePath($_application, Tinebase_Model_Container::TYPE_PERSONAL);
108 if (!$this->fileExists($personalBasePath)) {
109 $this->mkdir($personalBasePath);
114 * get application base path
116 * @param Tinebase_Model_Application|string $_application
117 * @param string $_type
120 public function getApplicationBasePath($_application, $_type = NULL)
122 $application = $_application instanceof Tinebase_Model_Application
124 : Tinebase_Application::getInstance()->getApplicationById($_application);
126 $result = '/' . $application->getId();
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.');
133 $result .= '/folders/' . $_type;
140 * Get one tree node (by id)
142 * @param integer|Tinebase_Record_Interface $_id
143 * @param $_getDeleted get deleted records
144 * @return Tinebase_Model_Tree_Node
146 public function get($_id, $_getDeleted = FALSE)
148 $node = $this->_treeNodeBackend->get($_id, $_getDeleted);
149 $fileObject = $this->_fileObjectBackend->get($node->object_id);
150 $node->description = $fileObject->description;
156 * Get multiple tree nodes identified by id
158 * @param string|array $_id Ids
159 * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
161 public function getMultipleTreeNodes($_id)
163 return $this->_treeNodeBackend->getMultiple($_id);
167 * create container node
169 * @param Tinebase_Model_Container $container
171 public function createContainerNode(Tinebase_Model_Container $container)
173 $path = $this->getContainerPath($container);
175 if (!$this->fileExists($path)) {
183 * @param Tinebase_Model_Container $container
186 public function getContainerPath(Tinebase_Model_Container $container)
188 $treeNodePath = new Tinebase_Model_Tree_Node_Path(array(
189 'application' => Tinebase_Application::getInstance()->getApplicationById($container->application_id)
191 $treeNodePath->setContainer($container);
193 return $treeNodePath->statpath;
199 * @param string $path if given, only remove this path from statcache
201 public function clearStatCache($path = NULL)
203 if ($path !== NULL) {
204 unset($this->_statCache[$this->_getCacheId($path)]);
206 // clear the whole cache
207 $this->_statCache = array();
212 * copy file/directory
214 * @todo copy recursive
216 * @param string $sourcePath
217 * @param string $destinationPath
218 * @throws Tinebase_Exception_UnexpectedValue
219 * @return Tinebase_Model_Tree_Node
221 public function copy($sourcePath, $destinationPath)
223 $destinationNode = $this->stat($sourcePath);
224 $sourcePathParts = $this->_splitPath($sourcePath);
227 // does destinationPath exist ...
228 $parentNode = $this->stat($destinationPath);
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.");
235 $destinationNodeName = basename(trim($sourcePath, '/'));
236 $destinationPathParts = array_merge($this->_splitPath($destinationPath), (array)$destinationNodeName);
238 } catch (Tinebase_Exception_NotFound $tenf) {
239 // does parent directory of destinationPath exist?
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.");
246 $destinationNodeName = basename(trim($destinationPath, '/'));
247 $destinationPathParts = array_merge($this->_splitPath(dirname($destinationPath)), (array)$destinationNodeName);
250 if ($sourcePathParts == $destinationPathParts) {
251 throw new Tinebase_Exception_UnexpectedValue("Source path and destination path must be different.");
254 // set new node properties
255 $destinationNode->setId(null);
256 $destinationNode->parent_id = $parentNode->getId();
257 $destinationNode->name = $destinationNodeName;
259 $createdNode = $this->_treeNodeBackend->create($destinationNode);
261 // update hash of all parent folders
262 $this->_updateDirectoryNodesHash(dirname(implode('/', $destinationPathParts)));
268 * get modification timestamp
270 * @param string $path
271 * @return string UNIX timestamp
273 public function getMTime($path)
275 $node = $this->stat($path);
277 $timestamp = $node->last_modified_time instanceof Tinebase_DateTime
278 ? $node->last_modified_time->getTimestamp()
279 : $node->creation_time->getTimestamp();
285 * check if file exists
287 * @param string $path
288 * @return boolean true if file/directory exists
290 public function fileExists($path)
294 } catch (Tinebase_Exception_NotFound $tenf) {
304 * @param handle $handle
307 public function fclose($handle)
309 if (!is_resource($handle)) {
313 $options = stream_context_get_options($handle);
315 switch ($options['tine20']['mode']) {
320 list ($hash, $hashFile) = $this->createFileBlob($handle);
322 $this->_updateFileObject($options['tine20']['node']->object_id, $hash, $hashFile);
324 $this->clearStatCache($options['tine20']['path']);
326 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Writing to file : ' . $options['tine20']['path'] . ' successful.');
331 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Got mode : ' . $options['tine20']['mode'] . ' - nothing to do.');
336 // update hash of all parent folders
337 $this->_updateDirectoryNodesHash(dirname($options['tine20']['path']));
343 * update file object with hash file info
346 * @param string $_hash
347 * @param string $_hashFile
348 * @return Tinebase_Model_Tree_FileObject
350 protected function _updateFileObject($_id, $_hash, $_hashFile = null)
352 $currentFileObject = $_id instanceof Tinebase_Record_Abstract ? $_id : $this->_fileObjectBackend->get($_id);
354 $_hashFile = $_hashFile ?: ($this->_basePath . '/' . substr($_hash, 0, 3) . '/' . substr($_hash, 3));
356 $updatedFileObject = clone($currentFileObject);
357 $updatedFileObject->hash = $_hash;
358 $updatedFileObject->size = filesize($_hashFile);
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;
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.');
372 $modLog = Tinebase_Timemachine_ModificationLog::getInstance();
373 $modLog->setRecordMetaData($updatedFileObject, 'update', $currentFileObject);
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}");
384 // sanitize file size, somehow filesize() seems to return empty strings on some systems
385 if (empty($updatedFileObject->size)) {
386 $updatedFileObject->size = 0;
389 return $this->_fileObjectBackend->update($updatedFileObject);
393 * update hash of all directories for given path
395 * @param string $path
397 protected function _updateDirectoryNodesHash($path)
399 // update hash of all parent folders
400 $parentNodes = $this->_getPathNodes($path);
401 $updatedNodes = $this->_fileObjectBackend->updateDirectoryNodesHash($parentNodes);
403 // update nodes stored in local statCache
405 foreach ($parentNodes as $node) {
406 $directoryObject = $updatedNodes->getById($node->object_id);
408 if ($directoryObject) {
409 $node->revision = $directoryObject->revision;
410 $node->hash = $directoryObject->hash;
413 $subPath .= "/" . $node->name;
414 $this->_addStatCache($subPath, $node);
421 * @param string $_path
422 * @param string $_mode
425 public function fopen($_path, $_mode)
427 $dirName = dirname($_path);
428 $fileName = basename($_path);
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.
437 if (!$this->isDir($dirName) || $this->fileExists($_path)) {
441 $parent = $this->stat($dirName);
442 $node = $this->createFileTreeNode($parent, $fileName);
444 $handle = Tinebase_TempFile::getInstance()->openTempFile();
448 // Open for reading only; place the file pointer at the beginning of the file.
451 if ($this->isDir($_path) || !$this->fileExists($_path)) {
455 $node = $this->stat($_path);
456 $hashFile = $this->_basePath . '/' . substr($node->hash, 0, 3) . '/' . substr($node->hash, 3);
458 $handle = fopen($hashFile, $_mode);
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.
466 if (!$this->isDir($dirName)) {
470 if (!$this->fileExists($_path)) {
471 $parent = $this->stat($dirName);
472 $node = $this->createFileTreeNode($parent, $fileName);
474 $node = $this->stat($_path);
477 $handle = Tinebase_TempFile::getInstance()->openTempFile();
485 $contextOptions = array('tine20' => array(
490 stream_context_set_option($handle, $contextOptions);
498 * @deprecated use Tinebase_FileSystem::stat()->contenttype
499 * @param string $path
502 public function getContentType($path)
504 $node = $this->stat($path);
506 return $node->contenttype;
512 * @deprecated use Tinebase_FileSystem::stat()->hash
513 * @param string $path
516 public function getETag($path)
518 $node = $this->stat($path);
524 * return if path is a directory
526 * @param string $path
529 public function isDir($path)
532 $node = $this->stat($path);
533 } catch (Tinebase_Exception_InvalidArgument $teia) {
535 } catch (Tinebase_Exception_NotFound $tenf) {
539 if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
547 * return if path is a file
549 * @param string $path
552 public function isFile($path)
555 $node = $this->stat($path);
556 } catch (Tinebase_Exception_InvalidArgument $teia) {
558 } catch (Tinebase_Exception_NotFound $tenf) {
562 if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FILE) {
570 * rename file/directory
572 * @param string $oldPath
573 * @param string $newPath
574 * @return Tinebase_Model_Tree_Node
576 public function rename($oldPath, $newPath)
579 $node = $this->stat($oldPath);
580 } catch (Tinebase_Exception_InvalidArgument $teia) {
582 } catch (Tinebase_Exception_NotFound $tenf) {
586 if (dirname($oldPath) != dirname($newPath)) {
588 $newParent = $this->stat(dirname($newPath));
589 } catch (Tinebase_Exception_InvalidArgument $teia) {
591 } catch (Tinebase_Exception_NotFound $tenf) {
595 $node->parent_id = $newParent->getId();
598 if (basename($oldPath) != basename($newPath)) {
599 $node->name = basename($newPath);
602 $node = $this->_treeNodeBackend->update($node);
604 $this->clearStatCache($oldPath);
606 $this->_addStatCache($newPath, $node);
614 * @param string $path
616 public function mkdir($path)
618 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
619 . ' Creating directory ' . $path);
621 $currentPath = array();
623 $pathParts = $this->_splitPath($path);
625 foreach ($pathParts as $pathPart) {
626 $pathPart = trim($pathPart);
627 $currentPath[]= $pathPart;
630 $node = $this->stat('/' . implode('/', $currentPath));
631 } catch (Tinebase_Exception_NotFound $tenf) {
632 $node = $this->createDirectoryTreeNode($parentNode, $pathPart);
634 $this->_addStatCache($currentPath, $node);
640 // update hash of all parent folders
641 $this->_updateDirectoryNodesHash($path);
649 * @param string $path
650 * @param boolean $recursive
653 public function rmdir($path, $recursive = FALSE)
655 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
656 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Removing directory ' . $path);
658 $node = $this->stat($path);
660 $children = $this->getTreeNodeChildren($node);
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');
667 foreach ($children as $child) {
668 if ($this->isDir($path . '/' . $child->name)) {
669 $this->rmdir($path . '/' . $child->name, true);
671 $this->unlink($path . '/' . $child->name);
677 $this->_treeNodeBackend->delete($node->getId());
678 $this->clearStatCache($path);
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);
691 * @param string $path
692 * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
694 public function scanDir($path)
696 $children = $this->getTreeNodeChildren($this->stat($path));
698 foreach ($children as $node) {
699 $this->_addStatCache($path . '/' . $node->name, $node);
706 * @param string $path
707 * @return Tinebase_Model_Tree_Node
709 public function stat($path)
711 $pathParts = $this->_splitPath($path);
712 $cacheId = $this->_getCacheId($pathParts);
714 // let's see if the path is cached in statCache
715 if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
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();
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);
732 if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
733 $parentNode = $this->_statCache[$cacheId];
738 $missingPathParts = array_diff_assoc($this->_splitPath($path), $pathParts);
740 foreach ($missingPathParts as $pathPart) {
741 $node = $this->_treeNodeBackend->getChild($parentNode, $pathPart);
743 // keep track of current path position
744 array_push($pathParts, $pathPart);
746 // add found path to statCache
747 $this->_addStatCache($pathParts, $node);
758 * @deprecated use Tinebase_FileSystem::stat()->size
759 * @param string $path
762 public function filesize($path)
764 $node = $this->stat($path);
772 * @param string $_path
775 public function unlink($path)
777 $node = $this->stat($path);
778 $this->deleteFileNode($node);
780 $this->clearStatCache($path);
782 // update hash of all parent folders
783 $this->_updateDirectoryNodesHash(dirname($path));
791 * @param Tinebase_Model_Tree_Node $node
793 public function deleteFileNode(Tinebase_Model_Tree_Node $node)
795 if ($node->type == Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
796 throw new Tinebase_Exception_InvalidArgument('can not unlink directories');
799 $this->_treeNodeBackend->delete($node->getId());
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);
810 * @param string|Tinebase_Model_Tree_Node $parentId
811 * @param string $name
812 * @return Tinebase_Model_Tree_Node
814 public function createDirectoryTreeNode($parentId, $name)
816 $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
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(),
824 Tinebase_Timemachine_ModificationLog::setRecordMetaData($directoryObject, 'create');
825 $directoryObject = $this->_fileObjectBackend->create($directoryObject);
827 $treeNode = new Tinebase_Model_Tree_Node(array(
829 'object_id' => $directoryObject->getId(),
830 'parent_id' => $parentId
832 $treeNode = $this->_treeNodeBackend->create($treeNode);
838 * create new file node
840 * @param string|Tinebase_Model_Tree_Node $parentId
841 * @param string $name
842 * @throws Tinebase_Exception_InvalidArgument
843 * @return Tinebase_Model_Tree_Node
845 public function createFileTreeNode($parentId, $name)
847 $parentId = $parentId instanceof Tinebase_Model_Tree_Node ? $parentId->getId() : $parentId;
849 $fileObject = new Tinebase_Model_Tree_FileObject(array(
850 'type' => Tinebase_Model_Tree_FileObject::TYPE_FILE,
851 'contentytype' => null,
853 Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'create');
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}");
865 $fileObject = $this->_fileObjectBackend->create($fileObject);
867 $treeNode = new Tinebase_Model_Tree_Node(array(
869 'object_id' => $fileObject->getId(),
870 'parent_id' => $parentId
873 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
874 ' ' . print_r($treeNode->toArray(), TRUE));
876 $treeNode = $this->_treeNodeBackend->create($treeNode);
882 * places contents into a file blob
884 * @param stream|string|tempFile $contents
885 * @return string hash
887 public function createFileBlob($contents)
889 if (! is_resource($contents)) {
890 throw new Tinebase_Exception_NotImplemented('please implement me!');
896 $ctx = hash_init('sha1');
897 hash_update_stream($ctx, $handle);
898 $hash = hash_final($ctx);
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');
908 $hashFile = $hashDirectory . '/' . substr($hash, 3);
909 if (!file_exists($hashFile)) {
910 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash file: ' . $hashFile);
912 $hashHandle = fopen($hashFile, 'x');
913 stream_copy_to_stream($handle, $hashHandle);
917 return array($hash, $hashFile);
921 * get tree node children
923 * @param string|Tinebase_Model_Tree_Node|Tinebase_Record_RecordSet $nodeId
924 * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
926 public function getTreeNodeChildren($nodeId)
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();
936 $operator = 'equals';
939 $searchFilter = new Tinebase_Model_Tree_Node_Filter(array(
941 'field' => 'parent_id',
942 'operator' => $operator,
946 $children = $this->searchNodes($searchFilter);
954 * @param Tinebase_Model_Tree_Node_Filter $_filter
955 * @param Tinebase_Record_Interface $_pagination
956 * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
958 public function searchNodes(Tinebase_Model_Tree_Node_Filter $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL)
960 $result = $this->_treeNodeBackend->search($_filter, $_pagination);
965 * search tree nodes count
967 * @param Tinebase_Model_Tree_Node_Filter $_filter
970 public function searchNodesCount(Tinebase_Model_Tree_Node_Filter $_filter = NULL)
972 $result = $this->_treeNodeBackend->searchCount($_filter);
977 * get nodes by container (or container id)
979 * @param int|Tinebase_Model_Container $container
980 * @return Tinebase_Record_RecordSet
982 public function getNodesByContainer($container)
984 $nodeContainer = ($container instanceof Tinebase_Model_Container) ? $container : Tinebase_Container::getInstance()->getContainerById($container);
985 $path = $this->getContainerPath($nodeContainer);
986 $parentNode = $this->stat($path);
987 $filter = new Tinebase_Model_Tree_Node_Filter(array(
988 array('field' => 'parent_id', 'operator' => 'equals', 'value' => $parentNode->getId())
991 return $this->searchNodes($filter);
995 * get tree node specified by parent node (or id) and name
997 * @param string|Tinebase_Model_Tree_Node $_parentId
998 * @param string $_name
999 * @throws Tinebase_Exception_InvalidArgument
1000 * @return Tinebase_Model_Tree_Node
1002 public function getTreeNode($_parentId, $_name)
1004 $parentId = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId->getId() : $_parentId;
1006 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1007 . ' Getting tree node ' . $parentId . '/'. $_name);
1009 return $this->_treeNodeBackend->getChild($_parentId, $_name);
1013 * add entry to stat cache
1015 * @param string|array $path
1016 * @param Tinebase_Model_Tree_Node $node
1018 protected function _addStatCache($path, Tinebase_Model_Tree_Node $node)
1020 $this->_statCache[$this->_getCacheId($path)] = $node;
1026 * @param string|array $path
1029 protected function _getCacheId($path)
1031 $pathParts = is_array($path) ? $path : $this->_splitPath($path);
1033 return sha1(implode(null, $pathParts));
1039 * @param string $path
1042 protected function _splitPath($path)
1044 return explode('/', trim($path, '/'));
1050 * @param Tinebase_Model_Tree_Node $_node
1051 * @return Tinebase_Model_Tree_Node
1053 public function update(Tinebase_Model_Tree_Node $_node)
1055 $currentNodeObject = $this->get($_node->getId());
1056 $fileObject = $this->_fileObjectBackend->get($currentNodeObject->object_id);
1058 Tinebase_Timemachine_ModificationLog::setRecordMetaData($_node, 'update', $currentNodeObject);
1059 Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'update', $fileObject);
1061 // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
1062 if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
1063 $fileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1064 header('X-OC-MTime: accepted');
1065 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
1066 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$fileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$_node->name}");
1070 // update file object
1071 $fileObject->description = $_node->description;
1072 $this->_updateFileObject($fileObject, $_node->hash);
1074 return $this->_treeNodeBackend->update($_node);
1078 * get container of node
1080 * @param Tinebase_Model_Tree_Node|string $node
1081 * @return Tinebase_Model_Container
1083 public function getNodeContainer($node)
1085 $nodesPath = $this->getPathOfNode($node);
1087 if (count($nodesPath) < 4) {
1088 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1089 ' ' . print_r($nodesPath[0], TRUE));
1090 throw new Tinebase_Exception_NotFound('Could not find container for node ' . $nodesPath[0]['id']);
1093 $containerNode = ($nodesPath[2]['name'] === Tinebase_Model_Container::TYPE_PERSONAL) ? $nodesPath[4] : $nodesPath[3];
1094 return Tinebase_Container::getInstance()->get($containerNode['name']);
1100 * @param Tinebase_Model_Tree_Node|string $node
1101 * @param boolean $getPathAsString
1102 * @return array|string
1104 public function getPathOfNode($node, $getPathAsString = FALSE)
1106 $node = $node instanceof Tinebase_Model_Tree_Node ? $node : $this->get($node);
1108 $nodesPath = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($node));
1109 while ($node->parent_id) {
1110 $node = $this->get($node->parent_id);
1111 $nodesPath->addRecord($node);
1114 $result = ($getPathAsString) ? '/' . implode('/', array_reverse($nodesPath->name)) : array_reverse($nodesPath->toArray());
1118 protected function _getPathNodes($path)
1120 $pathParts = $this->_splitPath($path);
1122 if (empty($pathParts)) {
1123 throw new Tinebase_Exception_InvalidArgument('empty path provided');
1127 $pathNodes = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
1129 foreach ($pathParts as $pathPart) {
1130 $subPath .= "/$pathPart";
1132 $node = $this->stat($subPath);
1134 $pathNodes->addRecord($node);
1142 * clears deleted files from filesystem + database
1144 public function clearDeletedFiles()
1146 $this->clearDeletedFilesFromFilesystem();
1147 $this->clearDeletedFilesFromDatabase();
1151 * removes deleted files that no longer exist in the database from the filesystem
1153 * @return integer number of deleted files
1155 public function clearDeletedFilesFromFilesystem()
1158 $dirIterator = new DirectoryIterator($this->_basePath);
1159 } catch (Exception $e) {
1160 throw new Tinebase_Exception_AccessDenied('Could not open files directory.');
1163 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1164 . ' Scanning ' . $this->_basePath . ' for deleted files ...');
1167 foreach ($dirIterator as $item) {
1168 $subDir = $item->getFileName();
1169 if ($subDir[0] == '.') continue;
1170 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1171 . ' Checking ' . $subDir);
1172 $subDirIterator = new DirectoryIterator($this->_basePath . '/' . $subDir);
1173 $hashsToCheck = array();
1174 // loop dirs + check if files in dir are in tree_filerevisions
1175 foreach ($subDirIterator as $file) {
1176 if ($file->isFile()) {
1177 $hash = $subDir . $file->getFileName();
1178 $hashsToCheck[] = $hash;
1181 $existingHashes = $this->_fileObjectBackend->checkRevisions($hashsToCheck);
1182 $hashesToDelete = array_diff($hashsToCheck, $existingHashes);
1183 // remove from filesystem if not existing any more
1184 foreach ($hashesToDelete as $hashToDelete) {
1185 $filename = $this->_basePath . '/' . $subDir . '/' . substr($hashToDelete, 3);
1186 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1187 . ' Deleting ' . $filename);
1193 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1194 . ' Deleted ' . $deleteCount . ' obsolete file(s).');
1196 return $deleteCount;
1200 * removes deleted files that no longer exist in the filesystem from the database
1202 * @return integer number of deleted files
1204 public function clearDeletedFilesFromDatabase()
1206 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1207 . ' Scanning database for deleted files ...');
1209 // get all file objects from db and check filesystem existance
1210 $toDeleteIds = array();
1211 $fileObjects = $this->_fileObjectBackend->getAll();
1212 foreach ($fileObjects as $fileObject) {
1213 if ($fileObject->type == Tinebase_Model_Tree_FileObject::TYPE_FILE && $fileObject->hash && ! file_exists($fileObject->getFilesystemPath())) {
1214 $toDeleteIds[] = $fileObject->getId();
1218 $nodeIdsToDelete = $this->_treeNodeBackend->search(new Tinebase_Model_Tree_Node_Filter(array(array(
1219 'field' => 'object_id',
1221 'value' => $toDeleteIds
1222 ))), NULL, Tinebase_Backend_Sql_Abstract::IDCOL);
1224 $deleteCount = $this->_treeNodeBackend->delete($nodeIdsToDelete);
1225 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1226 . ' Removed ' . $deleteCount . ' obsolete filenode(s) from the database.');
1228 return $deleteCount;
1232 * copy tempfile data to file path
1234 * @param mixed $tempFile
1235 Tinebase_Model_Tree_Node with property hash, tempfile or stream
1236 Tinebase_Model_Tempfile tempfile
1237 string with tempFile id
1238 array with [id] => tempFile id (this is odd IMHO)
1239 stream stream ressource
1240 NULL create empty file
1241 * @param string $path
1242 * @throws Tinebase_Exception_AccessDenied
1244 public function copyTempfile($tempFile, $path)
1246 if ($tempFile === NULL) {
1247 $tempStream = fopen('php://memory', 'r');
1248 } else if (is_resource($tempFile)) {
1249 $tempStream = $tempFile;
1250 } else if (is_string($tempFile) || is_array($tempFile)) {
1251 $tempFile = Tinebase_TempFile::getInstance()->getTempFile($tempFile);
1252 return $this->copyTempfile($tempFile, $path);
1253 } else if ($tempFile instanceof Tinebase_Model_Tree_Node) {
1254 if (isset($tempFile->hash)) {
1255 $hashFile = $this->_basePath . '/' . substr($tempFile->hash, 0, 3) . '/' . substr($tempFile->hash, 3);
1256 $tempStream = fopen($hashFile, 'r');
1257 } else if (is_resource($tempFile->stream)) {
1258 $tempStream = $tempFile->stream;
1260 return $this->copyTempfile($tempFile->tempFile, $path);
1262 } else if ($tempFile instanceof Tinebase_Model_TempFile) {
1263 $tempStream = fopen($tempFile->path, 'r');
1265 throw new Tinebase_Exception_UnexpectedValue('unexpected tempfile value');
1268 return $this->copyStream($tempStream, $path);
1272 * copy stream data to file path
1275 * @param string $path
1276 * @throws Tinebase_Exception_AccessDenied
1277 * @throws Tinebase_Exception_UnexpectedValue
1279 public function copyStream($in, $path)
1281 if (! $handle = $this->fopen($path, 'w')) {
1282 throw new Tinebase_Exception_AccessDenied('Permission denied to create file (filename ' . $path . ')');
1285 if (! is_resource($in)) {
1286 throw new Tinebase_Exception_UnexpectedValue('source needs to be of type stream');
1289 if (is_resource($in) !== NULL) {
1290 $metaData = stream_get_meta_data($in);
1291 if (true === $metaData['seekable']) {
1294 stream_copy_to_stream($in, $handle);
1296 $this->clearStatCache($path);
1299 $this->fclose($handle);