diff options
author | Andreas Fischer <bantu@owncloud.com> | 2013-12-14 18:32:48 +0100 |
---|---|---|
committer | Andreas Fischer <bantu@owncloud.com> | 2013-12-14 18:32:48 +0100 |
commit | c205d8d1c957ab5421a5864d183257f596336d2d (patch) | |
tree | a097ff1232f1283074c2cbd589fa1fddac037391 /lib/private/files | |
parent | d73285c1869591da74c148b577d780a73313fe90 (diff) | |
parent | 77b68505c2164330803ce5d5dcbb9fd07438e18d (diff) | |
download | nextcloud-server-c205d8d1c957ab5421a5864d183257f596336d2d.tar.gz nextcloud-server-c205d8d1c957ab5421a5864d183257f596336d2d.zip |
Merge remote-tracking branch 'owncloud/master' into fixing-3417-master
* owncloud/master: (1989 commits)
[tx-robot] updated from transifex
dont try to register background jobs if we haven't upgraded yet
adjust test
coding style
coding style
On webdav sesssions, loginname was compared to username which does not need to match necessarily
rely only on php DateTime to parse the db datetime string
LDAP: fix method behind save button on advancend and expert tabs, fixes at least Home Folder setinng
Fix webroot for update page
Update 3rdparty ref
update 3rdparty
toggle select all checkbox
remove unneeded ; in comment
LDAP: the browser shall not autofill userdn and password, usually login credentials are inserted. fixes #6283
Add test for having utf8 filenames in the cache
fix fallback overwriting result of getHome
[tx-robot] updated from transifex
fix smbclient directory listing parser
cache the home folder of a User
Send "SET NAMES utf8" to MySQL for PHP below 5.3.6
...
Conflicts:
lib/util.php
Diffstat (limited to 'lib/private/files')
39 files changed, 8344 insertions, 0 deletions
diff --git a/lib/private/files/cache/backgroundwatcher.php b/lib/private/files/cache/backgroundwatcher.php new file mode 100644 index 00000000000..923804f48d0 --- /dev/null +++ b/lib/private/files/cache/backgroundwatcher.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +use \OC\Files\Mount; +use \OC\Files\Filesystem; + +class BackgroundWatcher { + static $folderMimetype = null; + + static private function getFolderMimetype() { + if (!is_null(self::$folderMimetype)) { + return self::$folderMimetype; + } + $sql = 'SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = ?'; + $result = \OC_DB::executeAudited($sql, array('httpd/unix-directory')); + $row = $result->fetchRow(); + return $row['id']; + } + + static private function checkUpdate($id) { + $cacheItem = Cache::getById($id); + if (is_null($cacheItem)) { + return; + } + list($storageId, $internalPath) = $cacheItem; + $mounts = Filesystem::getMountByStorageId($storageId); + + if (count($mounts) === 0) { + //if the storage we need isn't mounted on default, try to find a user that has access to the storage + $permissionsCache = new Permissions($storageId); + $users = $permissionsCache->getUsers($id); + if (count($users) === 0) { + return; + } + Filesystem::initMountPoints($users[0]); + $mounts = Filesystem::getMountByStorageId($storageId); + if (count($mounts) === 0) { + return; + } + } + $storage = $mounts[0]->getStorage(); + $watcher = new Watcher($storage); + $watcher->checkUpdate($internalPath); + } + + /** + * get the next fileid in the cache + * + * @param int $previous + * @param bool $folder + * @return int + */ + static private function getNextFileId($previous, $folder) { + if ($folder) { + $stmt = \OC_DB::prepare('SELECT `fileid` FROM `*PREFIX*filecache` WHERE `fileid` > ? AND `mimetype` = ? ORDER BY `fileid` ASC', 1); + } else { + $stmt = \OC_DB::prepare('SELECT `fileid` FROM `*PREFIX*filecache` WHERE `fileid` > ? AND `mimetype` != ? ORDER BY `fileid` ASC', 1); + } + $result = \OC_DB::executeAudited($stmt, array($previous,self::getFolderMimetype())); + if ($row = $result->fetchRow()) { + return $row['fileid']; + } else { + return 0; + } + } + + static public function checkNext() { + // check both 1 file and 1 folder, this way new files are detected quicker because there are less folders than files usually + $previousFile = \OC_Appconfig::getValue('files', 'backgroundwatcher_previous_file', 0); + $previousFolder = \OC_Appconfig::getValue('files', 'backgroundwatcher_previous_folder', 0); + $nextFile = self::getNextFileId($previousFile, false); + $nextFolder = self::getNextFileId($previousFolder, true); + \OC_Appconfig::setValue('files', 'backgroundwatcher_previous_file', $nextFile); + \OC_Appconfig::setValue('files', 'backgroundwatcher_previous_folder', $nextFolder); + if ($nextFile > 0) { + self::checkUpdate($nextFile); + } + if ($nextFolder > 0) { + self::checkUpdate($nextFolder); + } + } + + static public function checkAll() { + $previous = 0; + $next = 1; + while ($next != 0) { + $next = self::getNextFileId($previous, true); + self::checkUpdate($next); + } + $previous = 0; + $next = 1; + while ($next != 0) { + $next = self::getNextFileId($previous, false); + self::checkUpdate($next); + } + } +} diff --git a/lib/private/files/cache/cache.php b/lib/private/files/cache/cache.php new file mode 100644 index 00000000000..8e682a96b75 --- /dev/null +++ b/lib/private/files/cache/cache.php @@ -0,0 +1,602 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +/** + * Metadata cache for the filesystem + * + * don't use this class directly if you need to get metadata, use \OC\Files\Filesystem::getFileInfo instead + */ +class Cache { + const NOT_FOUND = 0; + const PARTIAL = 1; //only partial data available, file not cached in the database + const SHALLOW = 2; //folder in cache, but not all child files are completely scanned + const COMPLETE = 3; + + /** + * @var array partial data for the cache + */ + private $partial = array(); + + /** + * @var string + */ + private $storageId; + + /** + * @var Storage $storageCache + */ + private $storageCache; + + private static $mimetypeIds = array(); + private static $mimetypes = array(); + + /** + * @param \OC\Files\Storage\Storage|string $storage + */ + public function __construct($storage) { + if ($storage instanceof \OC\Files\Storage\Storage) { + $this->storageId = $storage->getId(); + } else { + $this->storageId = $storage; + } + if (strlen($this->storageId) > 64) { + $this->storageId = md5($this->storageId); + } + + $this->storageCache = new Storage($storage); + } + + public function getNumericStorageId() { + return $this->storageCache->getNumericId(); + } + + /** + * normalize mimetypes + * + * @param string $mime + * @return int + */ + public function getMimetypeId($mime) { + if (empty($mime)) { + // Can not insert empty string into Oracle NOT NULL column. + $mime = 'application/octet-stream'; + } + if (empty(self::$mimetypeIds)) { + $this->loadMimetypes(); + } + + if (!isset(self::$mimetypeIds[$mime])) { + try{ + $result = \OC_DB::executeAudited('INSERT INTO `*PREFIX*mimetypes`(`mimetype`) VALUES(?)', array($mime)); + self::$mimetypeIds[$mime] = \OC_DB::insertid('*PREFIX*mimetypes'); + self::$mimetypes[self::$mimetypeIds[$mime]] = $mime; + } + catch (\Doctrine\DBAL\DBALException $e){ + \OC_Log::write('core', 'Exception during mimetype insertion: ' . $e->getmessage(), \OC_Log::DEBUG); + return -1; + } + } + + return self::$mimetypeIds[$mime]; + } + + public function getMimetype($id) { + if (empty(self::$mimetypes)) { + $this->loadMimetypes(); + } + + return isset(self::$mimetypes[$id]) ? self::$mimetypes[$id] : null; + } + + public function loadMimetypes(){ + $result = \OC_DB::executeAudited('SELECT `id`, `mimetype` FROM `*PREFIX*mimetypes`', array()); + if ($result) { + while ($row = $result->fetchRow()) { + self::$mimetypeIds[$row['mimetype']] = $row['id']; + self::$mimetypes[$row['id']] = $row['mimetype']; + } + } + } + + /** + * get the stored metadata of a file or folder + * + * @param string/int $file + * @return array | false + */ + public function get($file) { + if (is_string($file) or $file == '') { + // normalize file + $file = $this->normalize($file); + + $where = 'WHERE `storage` = ? AND `path_hash` = ?'; + $params = array($this->getNumericStorageId(), md5($file)); + } else { //file id + $where = 'WHERE `fileid` = ?'; + $params = array($file); + } + $sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, + `storage_mtime`, `encrypted`, `unencrypted_size`, `etag` + FROM `*PREFIX*filecache` ' . $where; + $result = \OC_DB::executeAudited($sql, $params); + $data = $result->fetchRow(); + + //FIXME hide this HACK in the next database layer, or just use doctrine and get rid of MDB2 and PDO + //PDO returns false, MDB2 returns null, oracle always uses MDB2, so convert null to false + if ($data === null) { + $data = false; + } + + //merge partial data + if (!$data and is_string($file)) { + if (isset($this->partial[$file])) { + $data = $this->partial[$file]; + } + } else { + //fix types + $data['fileid'] = (int)$data['fileid']; + $data['size'] = (int)$data['size']; + $data['mtime'] = (int)$data['mtime']; + $data['storage_mtime'] = (int)$data['storage_mtime']; + $data['encrypted'] = (bool)$data['encrypted']; + $data['unencrypted_size'] = (int)$data['unencrypted_size']; + $data['storage'] = $this->storageId; + $data['mimetype'] = $this->getMimetype($data['mimetype']); + $data['mimepart'] = $this->getMimetype($data['mimepart']); + if ($data['storage_mtime'] == 0) { + $data['storage_mtime'] = $data['mtime']; + } + } + + return $data; + } + + /** + * get the metadata of all files stored in $folder + * + * @param string $folder + * @return array + */ + public function getFolderContents($folder) { + $fileId = $this->getId($folder); + if ($fileId > -1) { + $sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, + `storage_mtime`, `encrypted`, `unencrypted_size`, `etag` + FROM `*PREFIX*filecache` WHERE `parent` = ? ORDER BY `name` ASC'; + $result = \OC_DB::executeAudited($sql,array($fileId)); + $files = $result->fetchAll(); + foreach ($files as &$file) { + $file['mimetype'] = $this->getMimetype($file['mimetype']); + $file['mimepart'] = $this->getMimetype($file['mimepart']); + if ($file['storage_mtime'] == 0) { + $file['storage_mtime'] = $file['mtime']; + } + if ($file['encrypted']) { + $file['encrypted_size'] = $file['size']; + $file['size'] = $file['unencrypted_size']; + } + } + return $files; + } else { + return array(); + } + } + + /** + * store meta data for a file or folder + * + * @param string $file + * @param array $data + * + * @return int file id + */ + public function put($file, array $data) { + if (($id = $this->getId($file)) > -1) { + $this->update($id, $data); + return $id; + } else { + // normalize file + $file = $this->normalize($file); + + if (isset($this->partial[$file])) { //add any saved partial data + $data = array_merge($this->partial[$file], $data); + unset($this->partial[$file]); + } + + $requiredFields = array('size', 'mtime', 'mimetype'); + foreach ($requiredFields as $field) { + if (!isset($data[$field])) { //data not complete save as partial and return + $this->partial[$file] = $data; + return -1; + } + } + + $data['path'] = $file; + $data['parent'] = $this->getParentId($file); + $data['name'] = \OC_Util::basename($file); + + list($queryParts, $params) = $this->buildParts($data); + $queryParts[] = '`storage`'; + $params[] = $this->getNumericStorageId(); + $valuesPlaceholder = array_fill(0, count($queryParts), '?'); + + $sql = 'INSERT INTO `*PREFIX*filecache` (' . implode(', ', $queryParts) . ')' + . ' VALUES (' . implode(', ', $valuesPlaceholder) . ')'; + \OC_DB::executeAudited($sql, $params); + + return (int)\OC_DB::insertid('*PREFIX*filecache'); + } + } + + /** + * update the metadata in the cache + * + * @param int $id + * @param array $data + */ + public function update($id, array $data) { + + if(isset($data['path'])) { + // normalize path + $data['path'] = $this->normalize($data['path']); + } + + if(isset($data['name'])) { + // normalize path + $data['name'] = $this->normalize($data['name']); + } + + list($queryParts, $params) = $this->buildParts($data); + $params[] = $id; + + $sql = 'UPDATE `*PREFIX*filecache` SET ' . implode(' = ?, ', $queryParts) . '=? WHERE `fileid` = ?'; + \OC_DB::executeAudited($sql, $params); + } + + /** + * extract query parts and params array from data array + * + * @param array $data + * @return array + */ + function buildParts(array $data) { + $fields = array('path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted', 'unencrypted_size', 'etag'); + $params = array(); + $queryParts = array(); + foreach ($data as $name => $value) { + if (array_search($name, $fields) !== false) { + if ($name === 'path') { + $params[] = md5($value); + $queryParts[] = '`path_hash`'; + } elseif ($name === 'mimetype') { + $params[] = $this->getMimetypeId(substr($value, 0, strpos($value, '/'))); + $queryParts[] = '`mimepart`'; + $value = $this->getMimetypeId($value); + } elseif ($name === 'storage_mtime') { + if (!isset($data['mtime'])) { + $params[] = $value; + $queryParts[] = '`mtime`'; + } + } elseif ($name === 'encrypted') { + // Boolean to integer conversion + $value = $value ? 1 : 0; + } + $params[] = $value; + $queryParts[] = '`' . $name . '`'; + } + } + return array($queryParts, $params); + } + + /** + * get the file id for a file + * + * @param string $file + * @return int + */ + public function getId($file) { + // normalize file + $file = $this->normalize($file); + + $pathHash = md5($file); + + $sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?'; + $result = \OC_DB::executeAudited($sql, array($this->getNumericStorageId(), $pathHash)); + if ($row = $result->fetchRow()) { + return $row['fileid']; + } else { + return -1; + } + } + + /** + * get the id of the parent folder of a file + * + * @param string $file + * @return int + */ + public function getParentId($file) { + if ($file === '') { + return -1; + } else { + $parent = dirname($file); + if ($parent === '.') { + $parent = ''; + } + return $this->getId($parent); + } + } + + /** + * check if a file is available in the cache + * + * @param string $file + * @return bool + */ + public function inCache($file) { + return $this->getId($file) != -1; + } + + /** + * remove a file or folder from the cache + * + * @param string $file + */ + public function remove($file) { + $entry = $this->get($file); + if ($entry['mimetype'] === 'httpd/unix-directory') { + $children = $this->getFolderContents($file); + foreach ($children as $child) { + $this->remove($child['path']); + } + } + + $sql = 'DELETE FROM `*PREFIX*filecache` WHERE `fileid` = ?'; + \OC_DB::executeAudited($sql, array($entry['fileid'])); + + $permissionsCache = new Permissions($this->storageId); + $permissionsCache->remove($entry['fileid']); + } + + /** + * Move a file or folder in the cache + * + * @param string $source + * @param string $target + */ + public function move($source, $target) { + // normalize source and target + $source = $this->normalize($source); + $target = $this->normalize($target); + + $sourceData = $this->get($source); + $sourceId = $sourceData['fileid']; + $newParentId = $this->getParentId($target); + + if ($sourceData['mimetype'] === 'httpd/unix-directory') { + //find all child entries + $sql = 'SELECT `path`, `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path` LIKE ?'; + $result = \OC_DB::executeAudited($sql, array($this->getNumericStorageId(), $source . '/%')); + $childEntries = $result->fetchAll(); + $sourceLength = strlen($source); + $query = \OC_DB::prepare('UPDATE `*PREFIX*filecache` SET `path` = ?, `path_hash` = ? WHERE `fileid` = ?'); + + foreach ($childEntries as $child) { + $targetPath = $target . substr($child['path'], $sourceLength); + \OC_DB::executeAudited($query, array($targetPath, md5($targetPath), $child['fileid'])); + } + } + + $sql = 'UPDATE `*PREFIX*filecache` SET `path` = ?, `path_hash` = ?, `name` = ?, `parent` =? WHERE `fileid` = ?'; + \OC_DB::executeAudited($sql, array($target, md5($target), basename($target), $newParentId, $sourceId)); + } + + /** + * remove all entries for files that are stored on the storage from the cache + */ + public function clear() { + $sql = 'DELETE FROM `*PREFIX*filecache` WHERE `storage` = ?'; + \OC_DB::executeAudited($sql, array($this->getNumericStorageId())); + + $sql = 'DELETE FROM `*PREFIX*storages` WHERE `id` = ?'; + \OC_DB::executeAudited($sql, array($this->storageId)); + } + + /** + * @param string $file + * + * @return int, Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE + */ + public function getStatus($file) { + // normalize file + $file = $this->normalize($file); + + $pathHash = md5($file); + $sql = 'SELECT `size` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?'; + $result = \OC_DB::executeAudited($sql, array($this->getNumericStorageId(), $pathHash)); + if ($row = $result->fetchRow()) { + if ((int)$row['size'] === -1) { + return self::SHALLOW; + } else { + return self::COMPLETE; + } + } else { + if (isset($this->partial[$file])) { + return self::PARTIAL; + } else { + return self::NOT_FOUND; + } + } + } + + /** + * search for files matching $pattern + * + * @param string $pattern + * @return array of file data + */ + public function search($pattern) { + + // normalize pattern + $pattern = $this->normalize($pattern); + + $sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, `encrypted`, `unencrypted_size`, `etag` + FROM `*PREFIX*filecache` WHERE `name` LIKE ? AND `storage` = ?'; + $result = \OC_DB::executeAudited($sql, array($pattern, $this->getNumericStorageId())); + $files = array(); + while ($row = $result->fetchRow()) { + $row['mimetype'] = $this->getMimetype($row['mimetype']); + $row['mimepart'] = $this->getMimetype($row['mimepart']); + $files[] = $row; + } + return $files; + } + + /** + * search for files by mimetype + * + * @param string $mimetype + * @return array + */ + public function searchByMime($mimetype) { + if (strpos($mimetype, '/')) { + $where = '`mimetype` = ?'; + } else { + $where = '`mimepart` = ?'; + } + $sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, `encrypted`, `unencrypted_size`, `etag` + FROM `*PREFIX*filecache` WHERE ' . $where . ' AND `storage` = ?'; + $mimetype = $this->getMimetypeId($mimetype); + $result = \OC_DB::executeAudited($sql, array($mimetype, $this->getNumericStorageId())); + $files = array(); + while ($row = $result->fetchRow()) { + $row['mimetype'] = $this->getMimetype($row['mimetype']); + $row['mimepart'] = $this->getMimetype($row['mimepart']); + $files[] = $row; + } + return $files; + } + + /** + * update the folder size and the size of all parent folders + * + * @param $path + */ + public function correctFolderSize($path) { + $this->calculateFolderSize($path); + if ($path !== '') { + $parent = dirname($path); + if ($parent === '.' or $parent === '/') { + $parent = ''; + } + $this->correctFolderSize($parent); + } + } + + /** + * get the size of a folder and set it in the cache + * + * @param string $path + * @return int + */ + public function calculateFolderSize($path) { + $totalSize = 0; + $entry = $this->get($path); + if ($entry && $entry['mimetype'] === 'httpd/unix-directory') { + $id = $entry['fileid']; + $sql = 'SELECT SUM(`size`) AS f1, MIN(`size`) AS f2 FROM `*PREFIX*filecache` '. + 'WHERE `parent` = ? AND `storage` = ?'; + $result = \OC_DB::executeAudited($sql, array($id, $this->getNumericStorageId())); + if ($row = $result->fetchRow()) { + list($sum, $min) = array_values($row); + $sum = (int)$sum; + $min = (int)$min; + if ($min === -1) { + $totalSize = $min; + } else { + $totalSize = $sum; + } + if ($entry['size'] !== $totalSize) { + $this->update($id, array('size' => $totalSize)); + } + + } + } + return $totalSize; + } + + /** + * get all file ids on the files on the storage + * + * @return int[] + */ + public function getAll() { + $sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ?'; + $result = \OC_DB::executeAudited($sql, array($this->getNumericStorageId())); + $ids = array(); + while ($row = $result->fetchRow()) { + $ids[] = $row['fileid']; + } + return $ids; + } + + /** + * find a folder in the cache which has not been fully scanned + * + * If multiply incomplete folders are in the cache, the one with the highest id will be returned, + * use the one with the highest id gives the best result with the background scanner, since that is most + * likely the folder where we stopped scanning previously + * + * @return string|bool the path of the folder or false when no folder matched + */ + public function getIncomplete() { + $query = \OC_DB::prepare('SELECT `path` FROM `*PREFIX*filecache`' + . ' WHERE `storage` = ? AND `size` = -1 ORDER BY `fileid` DESC',1); + $result = \OC_DB::executeAudited($query, array($this->getNumericStorageId())); + if ($row = $result->fetchRow()) { + return $row['path']; + } else { + return false; + } + } + + /** + * get the storage id of the storage for a file and the internal path of the file + * + * @param int $id + * @return array, first element holding the storage id, second the path + */ + static public function getById($id) { + $sql = 'SELECT `storage`, `path` FROM `*PREFIX*filecache` WHERE `fileid` = ?'; + $result = \OC_DB::executeAudited($sql, array($id)); + if ($row = $result->fetchRow()) { + $numericId = $row['storage']; + $path = $row['path']; + } else { + return null; + } + + if ($id = Storage::getStorageId($numericId)) { + return array($id, $path); + } else { + return null; + } + } + + /** + * normalize the given path + * @param $path + * @return string + */ + public function normalize($path) { + + return \OC_Util::normalizeUnicode($path); + } +} diff --git a/lib/private/files/cache/homecache.php b/lib/private/files/cache/homecache.php new file mode 100644 index 00000000000..18dfbfe3191 --- /dev/null +++ b/lib/private/files/cache/homecache.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +class HomeCache extends Cache { + /** + * get the size of a folder and set it in the cache + * + * @param string $path + * @return int + */ + public function calculateFolderSize($path) { + if ($path !== '/' and $path !== '') { + return parent::calculateFolderSize($path); + } + + $totalSize = 0; + $entry = $this->get($path); + if ($entry && $entry['mimetype'] === 'httpd/unix-directory') { + $id = $entry['fileid']; + $sql = 'SELECT SUM(`size`) FROM `*PREFIX*filecache` ' . + 'WHERE `parent` = ? AND `storage` = ? AND `size` >= 0'; + $result = \OC_DB::executeAudited($sql, array($id, $this->getNumericStorageId())); + if ($row = $result->fetchRow()) { + list($sum) = array_values($row); + $totalSize = (int)$sum; + if ($entry['size'] !== $totalSize) { + $this->update($id, array('size' => $totalSize)); + } + } + } + return $totalSize; + } + + public function get($path) { + $data = parent::get($path); + if ($path === '' or $path === '/') { + // only the size of the "files" dir counts + $filesData = parent::get('files'); + + if (isset($filesData['size'])) { + $data['size'] = $filesData['size']; + } + } + return $data; + } +} diff --git a/lib/private/files/cache/legacy.php b/lib/private/files/cache/legacy.php new file mode 100644 index 00000000000..8eed1f67a5d --- /dev/null +++ b/lib/private/files/cache/legacy.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +/** + * Provide read only support for the old filecache + */ +class Legacy { + private $user; + + private $cacheHasItems = null; + + public function __construct($user) { + $this->user = $user; + } + + /** + * get the numbers of items in the legacy cache + * + * @return int + */ + function getCount() { + $sql = 'SELECT COUNT(`id`) AS `count` FROM `*PREFIX*fscache` WHERE `user` = ?'; + $result = \OC_DB::executeAudited($sql, array($this->user)); + if ($row = $result->fetchRow()) { + return $row['count']; + } else { + return 0; + } + } + + /** + * check if a legacy cache is present and holds items + * + * @return bool + */ + function hasItems() { + if (!is_null($this->cacheHasItems)) { + return $this->cacheHasItems; + } + try { + $query = \OC_DB::prepare('SELECT `id` FROM `*PREFIX*fscache` WHERE `user` = ?',1); + } catch (\Exception $e) { + $this->cacheHasItems = false; + return false; + } + try { + $result = $query->execute(array($this->user)); + } catch (\Exception $e) { + $this->cacheHasItems = false; + return false; + } + + if ($result === false || property_exists($result, 'error_message_prefix')) { + $this->cacheHasItems = false; + return false; + } + + $this->cacheHasItems = (bool)$result->fetchRow(); + return $this->cacheHasItems; + } + + /** + * get an item from the legacy cache + * + * @param string|int $path + * @return array + */ + function get($path) { + if (is_numeric($path)) { + $sql = 'SELECT * FROM `*PREFIX*fscache` WHERE `id` = ?'; + } else { + $sql = 'SELECT * FROM `*PREFIX*fscache` WHERE `path` = ?'; + } + $result = \OC_DB::executeAudited($sql, array($path)); + $data = $result->fetchRow(); + $data['etag'] = $this->getEtag($data['path'], $data['user']); + return $data; + } + + /** + * Get the ETag for the given path + * + * @param type $path + * @return string + */ + function getEtag($path, $user = null) { + static $query = null; + + $pathDetails = explode('/', $path, 4); + if((!$user) && !isset($pathDetails[1])) { + //no user!? Too odd, return empty string. + return ''; + } else if(!$user) { + //guess user from path, if no user passed. + $user = $pathDetails[1]; + } + + if(!isset($pathDetails[3]) || is_null($pathDetails[3])) { + $relativePath = ''; + } else { + $relativePath = $pathDetails[3]; + } + + if(is_null($query)){ + $query = \OC_DB::prepare('SELECT `propertyvalue` FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = \'{DAV:}getetag\''); + } + $result = \OC_DB::executeAudited($query,array($user, '/' . $relativePath)); + if ($row = $result->fetchRow()) { + return trim($row['propertyvalue'], '"'); + } else { + return ''; + } + } + + /** + * get all child items of an item from the legacy cache + * + * @param int $id + * @return array + */ + function getChildren($id) { + $result = \OC_DB::executeAudited('SELECT * FROM `*PREFIX*fscache` WHERE `parent` = ?', array($id)); + $data = $result->fetchAll(); + foreach ($data as $i => $item) { + $data[$i]['etag'] = $this->getEtag($item['path'], $item['user']); + } + return $data; + } +} diff --git a/lib/private/files/cache/permissions.php b/lib/private/files/cache/permissions.php new file mode 100644 index 00000000000..2e2bdb20b78 --- /dev/null +++ b/lib/private/files/cache/permissions.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +class Permissions { + /** + * @var string $storageId + */ + private $storageId; + + /** + * @param \OC\Files\Storage\Storage|string $storage + */ + public function __construct($storage) { + if ($storage instanceof \OC\Files\Storage\Storage) { + $this->storageId = $storage->getId(); + } else { + $this->storageId = $storage; + } + } + + /** + * get the permissions for a single file + * + * @param int $fileId + * @param string $user + * @return int (-1 if file no permissions set) + */ + public function get($fileId, $user) { + $sql = 'SELECT `permissions` FROM `*PREFIX*permissions` WHERE `user` = ? AND `fileid` = ?'; + $result = \OC_DB::executeAudited($sql, array($user, $fileId)); + if ($row = $result->fetchRow()) { + return $row['permissions']; + } else { + return -1; + } + } + + /** + * set the permissions of a file + * + * @param int $fileId + * @param string $user + * @param int $permissions + */ + public function set($fileId, $user, $permissions) { + if (self::get($fileId, $user) !== -1) { + $sql = 'UPDATE `*PREFIX*permissions` SET `permissions` = ? WHERE `user` = ? AND `fileid` = ?'; + } else { + $sql = 'INSERT INTO `*PREFIX*permissions`(`permissions`, `user`, `fileid`) VALUES(?, ?,? )'; + } + \OC_DB::executeAudited($sql, array($permissions, $user, $fileId)); + } + + /** + * get the permissions of multiply files + * + * @param int[] $fileIds + * @param string $user + * @return int[] + */ + public function getMultiple($fileIds, $user) { + if (count($fileIds) === 0) { + return array(); + } + $params = $fileIds; + $params[] = $user; + $inPart = implode(', ', array_fill(0, count($fileIds), '?')); + + $sql = 'SELECT `fileid`, `permissions` FROM `*PREFIX*permissions`' + . ' WHERE `fileid` IN (' . $inPart . ') AND `user` = ?'; + $result = \OC_DB::executeAudited($sql, $params); + $filePermissions = array(); + while ($row = $result->fetchRow()) { + $filePermissions[$row['fileid']] = $row['permissions']; + } + return $filePermissions; + } + + /** + * get the permissions for all files in a folder + * + * @param int $parentId + * @param string $user + * @return int[] + */ + public function getDirectoryPermissions($parentId, $user) { + $sql = 'SELECT `*PREFIX*permissions`.`fileid`, `permissions` + FROM `*PREFIX*permissions` + INNER JOIN `*PREFIX*filecache` ON `*PREFIX*permissions`.`fileid` = `*PREFIX*filecache`.`fileid` + WHERE `*PREFIX*filecache`.`parent` = ? AND `*PREFIX*permissions`.`user` = ?'; + + $result = \OC_DB::executeAudited($sql, array($parentId, $user)); + $filePermissions = array(); + while ($row = $result->fetchRow()) { + $filePermissions[$row['fileid']] = $row['permissions']; + } + return $filePermissions; + } + + /** + * remove the permissions for a file + * + * @param int $fileId + * @param string $user + */ + public function remove($fileId, $user = null) { + if (is_null($user)) { + \OC_DB::executeAudited('DELETE FROM `*PREFIX*permissions` WHERE `fileid` = ?', array($fileId)); + } else { + $sql = 'DELETE FROM `*PREFIX*permissions` WHERE `fileid` = ? AND `user` = ?'; + \OC_DB::executeAudited($sql, array($fileId, $user)); + } + } + + public function removeMultiple($fileIds, $user) { + $query = \OC_DB::prepare('DELETE FROM `*PREFIX*permissions` WHERE `fileid` = ? AND `user` = ?'); + foreach ($fileIds as $fileId) { + \OC_DB::executeAudited($query, array($fileId, $user)); + } + } + + /** + * get the list of users which have permissions stored for a file + * + * @param int $fileId + */ + public function getUsers($fileId) { + $sql = 'SELECT `user` FROM `*PREFIX*permissions` WHERE `fileid` = ?'; + $result = \OC_DB::executeAudited($sql, array($fileId)); + $users = array(); + while ($row = $result->fetchRow()) { + $users[] = $row['user']; + } + return $users; + } +} diff --git a/lib/private/files/cache/scanner.php b/lib/private/files/cache/scanner.php new file mode 100644 index 00000000000..a8c069ee99f --- /dev/null +++ b/lib/private/files/cache/scanner.php @@ -0,0 +1,294 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +use OC\Files\Filesystem; +use OC\Hooks\BasicEmitter; + +/** + * Class Scanner + * + * Hooks available in scope \OC\Files\Cache\Scanner: + * - scanFile(string $path, string $storageId) + * - scanFolder(string $path, string $storageId) + * - postScanFile(string $path, string $storageId) + * - postScanFolder(string $path, string $storageId) + * + * @package OC\Files\Cache + */ +class Scanner extends BasicEmitter { + /** + * @var \OC\Files\Storage\Storage $storage + */ + private $storage; + + /** + * @var string $storageId + */ + private $storageId; + + /** + * @var \OC\Files\Cache\Cache $cache + */ + private $cache; + + /** + * @var \OC\Files\Cache\Permissions $permissionsCache + */ + private $permissionsCache; + + const SCAN_RECURSIVE = true; + const SCAN_SHALLOW = false; + + const REUSE_ETAG = 1; + const REUSE_SIZE = 2; + + public function __construct(\OC\Files\Storage\Storage $storage) { + $this->storage = $storage; + $this->storageId = $this->storage->getId(); + $this->cache = $storage->getCache(); + $this->permissionsCache = $storage->getPermissionsCache(); + } + + /** + * get all the metadata of a file or folder + * * + * + * @param string $path + * @return array with metadata of the file + */ + public function getData($path) { + if (!$this->storage->isReadable($path)) { + //cant read, nothing we can do + \OCP\Util::writeLog('OC\Files\Cache\Scanner', "!!! Path '$path' is not readable !!!", \OCP\Util::DEBUG); + return null; + } + $data = array(); + $data['mimetype'] = $this->storage->getMimeType($path); + $data['mtime'] = $this->storage->filemtime($path); + if ($data['mimetype'] == 'httpd/unix-directory') { + $data['size'] = -1; //unknown + } else { + $data['size'] = $this->storage->filesize($path); + } + $data['etag'] = $this->storage->getETag($path); + $data['storage_mtime'] = $data['mtime']; + return $data; + } + + /** + * scan a single file and store it in the cache + * + * @param string $file + * @param int $reuseExisting + * @param bool $parentExistsInCache + * @return array with metadata of the scanned file + */ + public function scanFile($file, $reuseExisting = 0, $parentExistsInCache = false) { + if (!self::isPartialFile($file) + and !Filesystem::isFileBlacklisted($file) + ) { + $this->emit('\OC\Files\Cache\Scanner', 'scanFile', array($file, $this->storageId)); + \OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', array('path' => $file, 'storage' => $this->storageId)); + $data = $this->getData($file); + if ($data) { + if ($file and !$parentExistsInCache) { + $parent = dirname($file); + if ($parent === '.' or $parent === '/') { + $parent = ''; + } + if (!$this->cache->inCache($parent)) { + $this->scanFile($parent); + } + } + $newData = $data; + $cacheData = $this->cache->get($file); + if ($cacheData) { + if (isset($cacheData['fileid'])) { + $this->permissionsCache->remove($cacheData['fileid']); + } + if ($reuseExisting) { + // prevent empty etag + $etag = $cacheData['etag']; + $propagateETagChange = false; + if (empty($etag)) { + $etag = $data['etag']; + $propagateETagChange = true; + } + // only reuse data if the file hasn't explicitly changed + if (isset($data['mtime']) && isset($cacheData['mtime']) && $data['mtime'] === $cacheData['mtime']) { + if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) { + $data['size'] = $cacheData['size']; + } + if ($reuseExisting & self::REUSE_ETAG) { + $data['etag'] = $etag; + if ($propagateETagChange) { + $parent = $file; + while ($parent !== '') { + $parent = dirname($parent); + if ($parent === '.') { + $parent = ''; + } + $parentCacheData = $this->cache->get($parent); + $this->cache->update($parentCacheData['fileid'], array( + 'etag' => $this->storage->getETag($parent), + )); + } + } + } + } + // Only update metadata that has changed + $newData = array_diff_assoc($data, $cacheData); + if (isset($newData['etag'])) { + $cacheDataString = print_r($cacheData, true); + $dataString = print_r($data, true); + \OCP\Util::writeLog('OC\Files\Cache\Scanner', + "!!! No reuse of etag for '$file' !!! \ncache: $cacheDataString \ndata: $dataString", + \OCP\Util::DEBUG); + } + } + } + if (!empty($newData)) { + $this->cache->put($file, $newData); + $this->emit('\OC\Files\Cache\Scanner', 'postScanFile', array($file, $this->storageId)); + \OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', array('path' => $file, 'storage' => $this->storageId)); + } + } else { + $this->cache->remove($file); + } + return $data; + } + return null; + } + + /** + * scan a folder and all it's children + * + * @param string $path + * @param bool $recursive + * @param int $reuse + * @return int the size of the scanned folder or -1 if the size is unknown at this stage + */ + public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1) { + if ($reuse === -1) { + $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : 0; + } + $this->scanFile($path, $reuse); + return $this->scanChildren($path, $recursive, $reuse); + } + + /** + * scan all the files and folders in a folder + * + * @param string $path + * @param bool $recursive + * @param int $reuse + * @return int the size of the scanned folder or -1 if the size is unknown at this stage + */ + public function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1) { + if ($reuse === -1) { + $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : 0; + } + $this->emit('\OC\Files\Cache\Scanner', 'scanFolder', array($path, $this->storageId)); + $size = 0; + $childQueue = array(); + $existingChildren = array(); + if ($this->cache->inCache($path)) { + $children = $this->cache->getFolderContents($path); + foreach ($children as $child) { + $existingChildren[] = $child['name']; + } + } + $newChildren = array(); + if ($this->storage->is_dir($path) && ($dh = $this->storage->opendir($path))) { + $exceptionOccurred = false; + \OC_DB::beginTransaction(); + if (is_resource($dh)) { + while (($file = readdir($dh)) !== false) { + $child = ($path) ? $path . '/' . $file : $file; + if (!Filesystem::isIgnoredDir($file)) { + $newChildren[] = $file; + try { + $data = $this->scanFile($child, $reuse, true); + if ($data) { + if ($data['size'] === -1) { + if ($recursive === self::SCAN_RECURSIVE) { + $childQueue[] = $child; + } else { + $size = -1; + } + } else if ($size !== -1) { + $size += $data['size']; + } + } + } + catch (\Doctrine\DBAL\DBALException $ex){ + // might happen if inserting duplicate while a scanning + // process is running in parallel + // log and ignore + \OC_Log::write('core', 'Exception while scanning file "' . $child . '": ' . $ex->getMessage(), \OC_Log::DEBUG); + $exceptionOccurred = true; + } + } + } + } + $removedChildren = \array_diff($existingChildren, $newChildren); + foreach ($removedChildren as $childName) { + $child = ($path) ? $path . '/' . $childName : $childName; + $this->cache->remove($child); + } + \OC_DB::commit(); + if ($exceptionOccurred){ + // It might happen that the parallel scan process has already + // inserted mimetypes but those weren't available yet inside the transaction + // To make sure to have the updated mime types in such cases, + // we reload them here + $this->cache->loadMimetypes(); + } + + foreach ($childQueue as $child) { + $childSize = $this->scanChildren($child, self::SCAN_RECURSIVE, $reuse); + if ($childSize === -1) { + $size = -1; + } else { + $size += $childSize; + } + } + $this->cache->put($path, array('size' => $size)); + } + $this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', array($path, $this->storageId)); + return $size; + } + + /** + * @brief check if the file should be ignored when scanning + * NOTE: files with a '.part' extension are ignored as well! + * prevents unfinished put requests to be scanned + * @param String $file + * @return boolean + */ + public static function isPartialFile($file) { + if (pathinfo($file, PATHINFO_EXTENSION) === 'part') { + return true; + } + return false; + } + + /** + * walk over any folders that are not fully scanned yet and scan them + */ + public function backgroundScan() { + $lastPath = null; + while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) { + $this->scan($path, self::SCAN_RECURSIVE, self::REUSE_ETAG); + $this->cache->correctFolderSize($path); + $lastPath = $path; + } + } +} diff --git a/lib/private/files/cache/storage.php b/lib/private/files/cache/storage.php new file mode 100644 index 00000000000..5657cf06e12 --- /dev/null +++ b/lib/private/files/cache/storage.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +/** + * Class Storage + * + * cache storage specific data + * + * @package OC\Files\Cache + */ +class Storage { + private $storageId; + private $numericId; + + /** + * @param \OC\Files\Storage\Storage|string $storage + */ + public function __construct($storage) { + if ($storage instanceof \OC\Files\Storage\Storage) { + $this->storageId = $storage->getId(); + } else { + $this->storageId = $storage; + } + if (strlen($this->storageId) > 64) { + $this->storageId = md5($this->storageId); + } + + $sql = 'SELECT `numeric_id` FROM `*PREFIX*storages` WHERE `id` = ?'; + $result = \OC_DB::executeAudited($sql, array($this->storageId)); + if ($row = $result->fetchRow()) { + $this->numericId = $row['numeric_id']; + } else { + $sql = 'INSERT INTO `*PREFIX*storages` (`id`) VALUES(?)'; + \OC_DB::executeAudited($sql, array($this->storageId)); + $this->numericId = \OC_DB::insertid('*PREFIX*storages'); + } + } + + public function getNumericId() { + return $this->numericId; + } + + public static function getStorageId($numericId) { + + $sql = 'SELECT `id` FROM `*PREFIX*storages` WHERE `numeric_id` = ?'; + $result = \OC_DB::executeAudited($sql, array($numericId)); + if ($row = $result->fetchRow()) { + return $row['id']; + } else { + return null; + } + } + + public static function exists($storageId) { + if (strlen($storageId) > 64) { + $storageId = md5($storageId); + } + $sql = 'SELECT `numeric_id` FROM `*PREFIX*storages` WHERE `id` = ?'; + $result = \OC_DB::executeAudited($sql, array($storageId)); + if ($row = $result->fetchRow()) { + return true; + } else { + return false; + } + } +} diff --git a/lib/private/files/cache/updater.php b/lib/private/files/cache/updater.php new file mode 100644 index 00000000000..73bc30e538f --- /dev/null +++ b/lib/private/files/cache/updater.php @@ -0,0 +1,218 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +use OCP\Util; + +/** + * listen to filesystem hooks and change the cache accordingly + */ +class Updater { + + /** + * resolve a path to a storage and internal path + * + * @param string $path the relative path + * @return array consisting of the storage and the internal path + */ + static public function resolvePath($path) { + $view = \OC\Files\Filesystem::getView(); + return $view->resolvePath($path); + } + + /** + * perform a write update + * + * @param string $path the relative path of the file + */ + static public function writeUpdate($path) { + /** + * @var \OC\Files\Storage\Storage $storage + * @var string $internalPath + */ + list($storage, $internalPath) = self::resolvePath($path); + if ($storage) { + $cache = $storage->getCache($internalPath); + $scanner = $storage->getScanner($internalPath); + $scanner->scan($internalPath, Scanner::SCAN_SHALLOW); + $cache->correctFolderSize($internalPath); + self::correctFolder($path, $storage->filemtime($internalPath)); + self::correctParentStorageMtime($storage, $internalPath); + } + } + + /** + * perform a delete update + * + * @param string $path the relative path of the file + */ + static public function deleteUpdate($path) { + /** + * @var \OC\Files\Storage\Storage $storage + * @var string $internalPath + */ + list($storage, $internalPath) = self::resolvePath($path); + if ($storage) { + $parent = dirname($internalPath); + if ($parent === '.') { + $parent = ''; + } + $cache = $storage->getCache($internalPath); + $cache->remove($internalPath); + $cache->correctFolderSize($parent); + self::correctFolder($path, time()); + self::correctParentStorageMtime($storage, $internalPath); + } + } + + /** + * preform a rename update + * + * @param string $from the relative path of the source file + * @param string $to the relative path of the target file + */ + static public function renameUpdate($from, $to) { + /** + * @var \OC\Files\Storage\Storage $storageFrom + * @var \OC\Files\Storage\Storage $storageTo + * @var string $internalFrom + * @var string $internalTo + */ + list($storageFrom, $internalFrom) = self::resolvePath($from); + list($storageTo, $internalTo) = self::resolvePath($to); + if ($storageFrom && $storageTo) { + if ($storageFrom === $storageTo) { + $cache = $storageFrom->getCache($internalFrom); + $cache->move($internalFrom, $internalTo); + if (pathinfo($internalFrom, PATHINFO_EXTENSION) !== pathinfo($internalTo, PATHINFO_EXTENSION)) { + // redetect mime type change + $mimeType = $storageTo->getMimeType($internalTo); + $fileId = $storageTo->getCache()->getId($internalTo); + $storageTo->getCache()->update($fileId, array('mimetype' => $mimeType)); + } + $cache->correctFolderSize($internalFrom); + $cache->correctFolderSize($internalTo); + self::correctFolder($from, time()); + self::correctFolder($to, time()); + self::correctParentStorageMtime($storageFrom, $internalFrom); + self::correctParentStorageMtime($storageTo, $internalTo); + } else { + self::deleteUpdate($from); + self::writeUpdate($to); + } + } + } + + /** + * @brief get file owner and path + * @param string $filename + * @return array with the oweners uid and the owners path + */ + private static function getUidAndFilename($filename) { + + $uid = \OC\Files\Filesystem::getOwner($filename); + \OC\Files\Filesystem::initMountPoints($uid); + + if ($uid != \OCP\User::getUser()) { + $info = \OC\Files\Filesystem::getFileInfo($filename); + $ownerView = new \OC\Files\View('/' . $uid . '/files'); + $filename = $ownerView->getPath($info['fileid']); + } + return array($uid, '/files/' . $filename); + } + + /** + * Update the mtime and ETag of all parent folders + * + * @param string $path + * @param string $time + */ + static public function correctFolder($path, $time) { + if ($path !== '' && $path !== '/') { + + list($owner, $realPath) = self::getUidAndFilename(dirname($path)); + + /** + * @var \OC\Files\Storage\Storage $storage + * @var string $internalPath + */ + $view = new \OC\Files\View('/' . $owner); + + list($storage, $internalPath) = $view->resolvePath($realPath); + $cache = $storage->getCache(); + $id = $cache->getId($internalPath); + + while ($id !== -1) { + $cache->update($id, array('mtime' => $time, 'etag' => $storage->getETag($internalPath))); + if ($realPath !== '') { + $realPath = dirname($realPath); + if($realPath === DIRECTORY_SEPARATOR ) { + $realPath = ""; + } + // check storage for parent in case we change the storage in this step + list($storage, $internalPath) = $view->resolvePath($realPath); + $cache = $storage->getCache(); + $id = $cache->getId($internalPath); + } else { + $id = -1; + } + } + } + } + + /** + * update the storage_mtime of the parent + * + * @param \OC\Files\Storage\Storage $storage + * @param string $internalPath + */ + static private function correctParentStorageMtime($storage, $internalPath) { + $cache = $storage->getCache(); + $parentId = $cache->getParentId($internalPath); + $parent = dirname($internalPath); + if ($parentId != -1) { + $cache->update($parentId, array('storage_mtime' => $storage->filemtime($parent))); + } + } + + /** + * @param array $params + */ + static public function writeHook($params) { + self::writeUpdate($params['path']); + } + + /** + * @param array $params + */ + static public function touchHook($params) { + $path = $params['path']; + list($storage, $internalPath) = self::resolvePath($path); + $cache = $storage->getCache(); + $id = $cache->getId($internalPath); + if ($id !== -1) { + $cache->update($id, array('etag' => $storage->getETag($internalPath))); + } + self::writeUpdate($path); + } + + /** + * @param array $params + */ + static public function renameHook($params) { + self::renameUpdate($params['oldpath'], $params['newpath']); + } + + /** + * @param array $params + */ + static public function deleteHook($params) { + self::deleteUpdate($params['path']); + } +} diff --git a/lib/private/files/cache/upgrade.php b/lib/private/files/cache/upgrade.php new file mode 100644 index 00000000000..e3a46896cbf --- /dev/null +++ b/lib/private/files/cache/upgrade.php @@ -0,0 +1,235 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +class Upgrade { + /** + * @var Legacy $legacy + */ + private $legacy; + + private $numericIds = array(); + + private $mimeTypeIds = array(); + + /** + * @param Legacy $legacy + */ + public function __construct($legacy) { + $this->legacy = $legacy; + } + + /** + * Preform a upgrade a path and it's childs + * + * @param string $path + * @param bool $mode + */ + function upgradePath($path, $mode = Scanner::SCAN_RECURSIVE) { + if (!$this->legacy->hasItems()) { + return; + } + \OC_Hook::emit('\OC\Files\Cache\Upgrade', 'migrate_path', $path); + if ($row = $this->legacy->get($path)) { + $data = $this->getNewData($row); + if ($data) { + $this->insert($data); + $this->upgradeChilds($data['id'], $mode); + } + } + } + + /** + * upgrade all child elements of an item + * + * @param int $id + * @param bool $mode + */ + function upgradeChilds($id, $mode = Scanner::SCAN_RECURSIVE) { + $children = $this->legacy->getChildren($id); + foreach ($children as $child) { + $childData = $this->getNewData($child); + \OC_Hook::emit('\OC\Files\Cache\Upgrade', 'migrate_path', $child['path']); + if ($childData) { + $this->insert($childData); + if ($mode == Scanner::SCAN_RECURSIVE) { + $this->upgradeChilds($child['id']); + } + } + } + } + + /** + * insert data into the new cache + * + * @param array $data the data for the new cache + */ + function insert($data) { + static $insertQuery = null; + if(is_null($insertQuery)) { + $insertQuery = \OC_DB::prepare('INSERT INTO `*PREFIX*filecache` + ( `fileid`, `storage`, `path`, `path_hash`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, `encrypted`, `etag` ) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'); + } + if (!$this->inCache($data['storage'], $data['path_hash'], $data['id'])) { + \OC_DB::executeAudited($insertQuery, array($data['id'], $data['storage'], + $data['path'], $data['path_hash'], $data['parent'], $data['name'], + $data['mimetype'], $data['mimepart'], $data['size'], $data['mtime'], $data['encrypted'], $data['etag'])); + } + } + + /** + * check if an item is already in the new cache + * + * @param string $storage + * @param string $pathHash + * @param string $id + * @return bool + */ + function inCache($storage, $pathHash, $id) { + static $query = null; + if(is_null($query)) { + $query = \OC_DB::prepare('SELECT `fileid` FROM `*PREFIX*filecache` WHERE (`storage` = ? AND `path_hash` = ?) OR `fileid` = ?'); + } + $result = \OC_DB::executeAudited($query, array($storage, $pathHash, $id)); + return (bool)$result->fetchRow(); + } + + /** + * get the new data array from the old one + * + * @param array $data the data from the old cache + * Example data array + * Array + * ( + * [id] => 418 + * [path] => /tina/files/picture.jpg //relative to datadir + * [path_hash] => 66d4547e372888deed80b24fec9b192b + * [parent] => 234 + * [name] => picture.jpg + * [user] => tina + * [size] => 1265283 + * [ctime] => 1363909709 + * [mtime] => 1363909709 + * [mimetype] => image/jpeg + * [mimepart] => image + * [encrypted] => 0 + * [versioned] => 0 + * [writable] => 1 + * ) + * + * @return array + */ + function getNewData($data) { + //Make sure there is a path, otherwise we can do nothing. + if(!isset($data['path'])) { + return false; + } + $newData = $data; + /** + * @var \OC\Files\Storage\Storage $storage + * @var string $internalPath; + */ + list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($data['path']); + if ($storage) { + $newData['etag'] = $data['etag']; + $newData['path_hash'] = md5($internalPath); + $newData['path'] = $internalPath; + $newData['storage'] = $this->getNumericId($storage); + $newData['parent'] = ($internalPath === '') ? -1 : $data['parent']; + $newData['permissions'] = ($data['writable']) ? \OCP\PERMISSION_ALL : \OCP\PERMISSION_READ; + $newData['storage_object'] = $storage; + $newData['mimetype'] = $this->getMimetypeId($newData['mimetype'], $storage); + $newData['mimepart'] = $this->getMimetypeId($newData['mimepart'], $storage); + return $newData; + } else { + \OC_Log::write('core', 'Unable to migrate data from old cache for '.$data['path'].' because the storage was not found', \OC_Log::ERROR); + return false; + } + } + + /** + * get the numeric storage id + * + * @param \OC\Files\Storage\Storage $storage + * @return int + */ + function getNumericId($storage) { + $storageId = $storage->getId(); + if (!isset($this->numericIds[$storageId])) { + $cache = $storage->getCache(); + $this->numericIds[$storageId] = $cache->getNumericStorageId(); + } + return $this->numericIds[$storageId]; + } + + /** + * get the numeric id for a mimetype + * + * @param string $mimetype + * @param \OC\Files\Storage\Storage $storage + * @return int + */ + function getMimetypeId($mimetype, $storage) { + if (!isset($this->mimeTypeIds[$mimetype])) { + $cache = new Cache($storage); + $this->mimeTypeIds[$mimetype] = $cache->getMimetypeId($mimetype); + } + return $this->mimeTypeIds[$mimetype]; + } + + /** + * check if a cache upgrade is required for $user + * + * @param string $user + * @return bool + */ + static function needUpgrade($user) { + $cacheVersion = (int)\OCP\Config::getUserValue($user, 'files', 'cache_version', 4); + if ($cacheVersion < 5) { + $legacy = new \OC\Files\Cache\Legacy($user); + if ($legacy->hasItems()) { + return true; + } + self::upgradeDone($user); + } + + return false; + } + + /** + * mark the filecache as upgrade + * + * @param string $user + */ + static function upgradeDone($user) { + \OCP\Config::setUserValue($user, 'files', 'cache_version', 5); + } + + /** + * Does a "silent" upgrade, i.e. without an Event-Source as triggered + * on User-Login via Ajax. This method is called within the regular + * ownCloud upgrade. + * + * @param string $user a User ID + */ + public static function doSilentUpgrade($user) { + if(!self::needUpgrade($user)) { + return; + } + $legacy = new \OC\Files\Cache\Legacy($user); + if ($legacy->hasItems()) { + \OC_DB::beginTransaction(); + $upgrade = new \OC\Files\Cache\Upgrade($legacy); + $upgrade->upgradePath('/' . $user . '/files'); + \OC_DB::commit(); + } + \OC\Files\Cache\Upgrade::upgradeDone($user); + } +} diff --git a/lib/private/files/cache/watcher.php b/lib/private/files/cache/watcher.php new file mode 100644 index 00000000000..58f624c8990 --- /dev/null +++ b/lib/private/files/cache/watcher.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Cache; + +/** + * check the storage backends for updates and change the cache accordingly + */ +class Watcher { + /** + * @var \OC\Files\Storage\Storage $storage + */ + protected $storage; + + /** + * @var Cache $cache + */ + protected $cache; + + /** + * @var Scanner $scanner; + */ + protected $scanner; + + /** + * @param \OC\Files\Storage\Storage $storage + */ + public function __construct(\OC\Files\Storage\Storage $storage) { + $this->storage = $storage; + $this->cache = $storage->getCache(); + $this->scanner = $storage->getScanner(); + } + + /** + * check $path for updates + * + * @param string $path + * @return boolean true if path was updated, false otherwise + */ + public function checkUpdate($path) { + $cachedEntry = $this->cache->get($path); + if ($this->storage->hasUpdated($path, $cachedEntry['storage_mtime'])) { + if ($this->storage->is_dir($path)) { + $this->scanner->scan($path, Scanner::SCAN_SHALLOW); + } else { + $this->scanner->scanFile($path); + } + if ($cachedEntry['mimetype'] === 'httpd/unix-directory') { + $this->cleanFolder($path); + } + $this->cache->correctFolderSize($path); + return true; + } + return false; + } + + /** + * remove deleted files in $path from the cache + * + * @param string $path + */ + public function cleanFolder($path) { + $cachedContent = $this->cache->getFolderContents($path); + foreach ($cachedContent as $entry) { + if (!$this->storage->file_exists($entry['path'])) { + $this->cache->remove($entry['path']); + } + } + } +} diff --git a/lib/private/files/filesystem.php b/lib/private/files/filesystem.php new file mode 100644 index 00000000000..a83e9aa86d2 --- /dev/null +++ b/lib/private/files/filesystem.php @@ -0,0 +1,800 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +/** + * Class for abstraction of filesystem functions + * This class won't call any filesystem functions for itself but will pass them to the correct OC_Filestorage object + * this class should also handle all the file permission related stuff + * + * Hooks provided: + * read(path) + * write(path, &run) + * post_write(path) + * create(path, &run) (when a file is created, both create and write will be emitted in that order) + * post_create(path) + * delete(path, &run) + * post_delete(path) + * rename(oldpath,newpath, &run) + * post_rename(oldpath,newpath) + * copy(oldpath,newpath, &run) (if the newpath doesn't exists yes, copy, create and write will be emitted in that order) + * post_rename(oldpath,newpath) + * post_initMountPoints(user, user_dir) + * + * the &run parameter can be set to false to prevent the operation from occurring + */ + +namespace OC\Files; + +use OC\Files\Storage\Loader; +const SPACE_NOT_COMPUTED = -1; +const SPACE_UNKNOWN = -2; +const SPACE_UNLIMITED = -3; + +class Filesystem { + /** + * @var Mount\Manager $mounts + */ + private static $mounts; + + public static $loaded = false; + /** + * @var \OC\Files\View $defaultInstance + */ + static private $defaultInstance; + + + /** + * classname which used for hooks handling + * used as signalclass in OC_Hooks::emit() + */ + const CLASSNAME = 'OC_Filesystem'; + + /** + * signalname emitted before file renaming + * + * @param string $oldpath + * @param string $newpath + */ + const signal_rename = 'rename'; + + /** + * signal emitted after file renaming + * + * @param string $oldpath + * @param string $newpath + */ + const signal_post_rename = 'post_rename'; + + /** + * signal emitted before file/dir creation + * + * @param string $path + * @param bool $run changing this flag to false in hook handler will cancel event + */ + const signal_create = 'create'; + + /** + * signal emitted after file/dir creation + * + * @param string $path + * @param bool $run changing this flag to false in hook handler will cancel event + */ + const signal_post_create = 'post_create'; + + /** + * signal emits before file/dir copy + * + * @param string $oldpath + * @param string $newpath + * @param bool $run changing this flag to false in hook handler will cancel event + */ + const signal_copy = 'copy'; + + /** + * signal emits after file/dir copy + * + * @param string $oldpath + * @param string $newpath + */ + const signal_post_copy = 'post_copy'; + + /** + * signal emits before file/dir save + * + * @param string $path + * @param bool $run changing this flag to false in hook handler will cancel event + */ + const signal_write = 'write'; + + /** + * signal emits after file/dir save + * + * @param string $path + */ + const signal_post_write = 'post_write'; + + /** + * signal emits when reading file/dir + * + * @param string $path + */ + const signal_read = 'read'; + + /** + * signal emits when removing file/dir + * + * @param string $path + */ + const signal_delete = 'delete'; + + /** + * parameters definitions for signals + */ + const signal_param_path = 'path'; + const signal_param_oldpath = 'oldpath'; + const signal_param_newpath = 'newpath'; + + /** + * run - changing this flag to false in hook handler will cancel event + */ + const signal_param_run = 'run'; + + /** + * @var \OC\Files\Storage\Loader $loader + */ + private static $loader; + + /** + * @param callable $wrapper + */ + public static function addStorageWrapper($wrapper) { + self::getLoader()->addStorageWrapper($wrapper); + + $mounts = self::getMountManager()->getAll(); + foreach ($mounts as $mount) { + $mount->wrapStorage($wrapper); + } + } + + public static function getLoader() { + if (!self::$loader) { + self::$loader = new Loader(); + } + return self::$loader; + } + + public static function getMountManager() { + if (!self::$mounts) { + \OC_Util::setupFS(); + } + return self::$mounts; + } + + /** + * get the mountpoint of the storage object for a path + * ( note: because a storage is not always mounted inside the fakeroot, the + * returned mountpoint is relative to the absolute root of the filesystem + * and doesn't take the chroot into account ) + * + * @param string $path + * @return string + */ + static public function getMountPoint($path) { + if (!self::$mounts) { + \OC_Util::setupFS(); + } + $mount = self::$mounts->find($path); + if ($mount) { + return $mount->getMountPoint(); + } else { + return ''; + } + } + + /** + * get a list of all mount points in a directory + * + * @param string $path + * @return string[] + */ + static public function getMountPoints($path) { + if (!self::$mounts) { + \OC_Util::setupFS(); + } + $result = array(); + $mounts = self::$mounts->findIn($path); + foreach ($mounts as $mount) { + $result[] = $mount->getMountPoint(); + } + return $result; + } + + /** + * get the storage mounted at $mountPoint + * + * @param string $mountPoint + * @return \OC\Files\Storage\Storage + */ + public static function getStorage($mountPoint) { + if (!self::$mounts) { + \OC_Util::setupFS(); + } + $mount = self::$mounts->find($mountPoint); + return $mount->getStorage(); + } + + /** + * @param $id + * @return Mount\Mount[] + */ + public static function getMountByStorageId($id) { + if (!self::$mounts) { + \OC_Util::setupFS(); + } + return self::$mounts->findByStorageId($id); + } + + /** + * @param $id + * @return Mount\Mount[] + */ + public static function getMountByNumericId($id) { + if (!self::$mounts) { + \OC_Util::setupFS(); + } + return self::$mounts->findByNumericId($id); + } + + /** + * resolve a path to a storage and internal path + * + * @param string $path + * @return array consisting of the storage and the internal path + */ + static public function resolvePath($path) { + if (!self::$mounts) { + \OC_Util::setupFS(); + } + $mount = self::$mounts->find($path); + if ($mount) { + return array($mount->getStorage(), $mount->getInternalPath($path)); + } else { + return array(null, null); + } + } + + static public function init($user, $root) { + if (self::$defaultInstance) { + return false; + } + self::getLoader(); + self::$defaultInstance = new View($root); + + if (!self::$mounts) { + self::$mounts = new Mount\Manager(); + } + + //load custom mount config + self::initMountPoints($user); + + self::$loaded = true; + + return true; + } + + static public function initMounts() { + if (!self::$mounts) { + self::$mounts = new Mount\Manager(); + } + } + + /** + * Initialize system and personal mount points for a user + * + * @param string $user + */ + public static function initMountPoints($user = '') { + if ($user == '') { + $user = \OC_User::getUser(); + } + $parser = new \OC\ArrayParser(); + + $root = \OC_User::getHome($user); + + $userObject = \OC_User::getManager()->get($user); + + if (!is_null($userObject)) { + // check for legacy home id (<= 5.0.12) + if (\OC\Files\Cache\Storage::exists('local::' . $root . '/')) { + self::mount('\OC\Files\Storage\Home', array('user' => $userObject, 'legacy' => true), $user); + } + else { + self::mount('\OC\Files\Storage\Home', array('user' => $userObject), $user); + } + } + else { + self::mount('\OC\Files\Storage\Local', array('datadir' => $root), $user); + } + $datadir = \OC_Config::getValue("datadirectory", \OC::$SERVERROOT . "/data"); + + //move config file to it's new position + if (is_file(\OC::$SERVERROOT . '/config/mount.json')) { + rename(\OC::$SERVERROOT . '/config/mount.json', $datadir . '/mount.json'); + } + // Load system mount points + if (is_file(\OC::$SERVERROOT . '/config/mount.php') or is_file($datadir . '/mount.json')) { + if (is_file($datadir . '/mount.json')) { + $mountConfig = json_decode(file_get_contents($datadir . '/mount.json'), true); + } elseif (is_file(\OC::$SERVERROOT . '/config/mount.php')) { + $mountConfig = $parser->parsePHP(file_get_contents(\OC::$SERVERROOT . '/config/mount.php')); + } + if (isset($mountConfig['global'])) { + foreach ($mountConfig['global'] as $mountPoint => $options) { + self::mount($options['class'], $options['options'], $mountPoint); + } + } + if (isset($mountConfig['group'])) { + foreach ($mountConfig['group'] as $group => $mounts) { + if (\OC_Group::inGroup($user, $group)) { + foreach ($mounts as $mountPoint => $options) { + $mountPoint = self::setUserVars($user, $mountPoint); + foreach ($options as &$option) { + $option = self::setUserVars($user, $option); + } + self::mount($options['class'], $options['options'], $mountPoint); + } + } + } + } + if (isset($mountConfig['user'])) { + foreach ($mountConfig['user'] as $mountUser => $mounts) { + if ($mountUser === 'all' or strtolower($mountUser) === strtolower($user)) { + foreach ($mounts as $mountPoint => $options) { + $mountPoint = self::setUserVars($user, $mountPoint); + foreach ($options as &$option) { + $option = self::setUserVars($user, $option); + } + self::mount($options['class'], $options['options'], $mountPoint); + } + } + } + } + } + // Load personal mount points + if (is_file($root . '/mount.php') or is_file($root . '/mount.json')) { + if (is_file($root . '/mount.json')) { + $mountConfig = json_decode(file_get_contents($root . '/mount.json'), true); + } elseif (is_file($root . '/mount.php')) { + $mountConfig = $parser->parsePHP(file_get_contents($root . '/mount.php')); + } + if (isset($mountConfig['user'][$user])) { + foreach ($mountConfig['user'][$user] as $mountPoint => $options) { + self::mount($options['class'], $options['options'], $mountPoint); + } + } + } + + // Chance to mount for other storages + \OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', array('user' => $user, 'user_dir' => $root)); + } + + /** + * fill in the correct values for $user + * + * @param string $user + * @param string $input + * @return string + */ + private static function setUserVars($user, $input) { + return str_replace('$user', $user, $input); + } + + /** + * get the default filesystem view + * + * @return View + */ + static public function getView() { + return self::$defaultInstance; + } + + /** + * tear down the filesystem, removing all storage providers + */ + static public function tearDown() { + self::clearMounts(); + self::$defaultInstance = null; + } + + /** + * @brief get the relative path of the root data directory for the current user + * @return string + * + * Returns path like /admin/files + */ + static public function getRoot() { + return self::$defaultInstance->getRoot(); + } + + /** + * clear all mounts and storage backends + */ + public static function clearMounts() { + if (self::$mounts) { + self::$mounts->clear(); + } + } + + /** + * mount an \OC\Files\Storage\Storage in our virtual filesystem + * + * @param \OC\Files\Storage\Storage|string $class + * @param array $arguments + * @param string $mountpoint + */ + static public function mount($class, $arguments, $mountpoint) { + if (!self::$mounts) { + \OC_Util::setupFS(); + } + $mount = new Mount\Mount($class, $mountpoint, $arguments, self::getLoader()); + self::$mounts->addMount($mount); + } + + /** + * return the path to a local version of the file + * we need this because we can't know if a file is stored local or not from + * outside the filestorage and for some purposes a local file is needed + * + * @param string $path + * @return string + */ + static public function getLocalFile($path) { + return self::$defaultInstance->getLocalFile($path); + } + + /** + * @param string $path + * @return string + */ + static public function getLocalFolder($path) { + return self::$defaultInstance->getLocalFolder($path); + } + + /** + * return path to file which reflects one visible in browser + * + * @param string $path + * @return string + */ + static public function getLocalPath($path) { + $datadir = \OC_User::getHome(\OC_User::getUser()) . '/files'; + $newpath = $path; + if (strncmp($newpath, $datadir, strlen($datadir)) == 0) { + $newpath = substr($path, strlen($datadir)); + } + return $newpath; + } + + /** + * check if the requested path is valid + * + * @param string $path + * @return bool + */ + static public function isValidPath($path) { + $path = self::normalizePath($path); + if (!$path || $path[0] !== '/') { + $path = '/' . $path; + } + if (strstr($path, '/../') || strrchr($path, '/') === '/..') { + return false; + } + return true; + } + + /** + * checks if a file is blacklisted for storage in the filesystem + * Listens to write and rename hooks + * + * @param array $data from hook + */ + static public function isBlacklisted($data) { + if (isset($data['path'])) { + $path = $data['path']; + } else if (isset($data['newpath'])) { + $path = $data['newpath']; + } + if (isset($path)) { + if (self::isFileBlacklisted($path)) { + $data['run'] = false; + } + } + } + + /** + * @param string $filename + * @return bool + */ + static public function isFileBlacklisted($filename) { + $blacklist = \OC_Config::getValue('blacklisted_files', array('.htaccess')); + $filename = strtolower(basename($filename)); + return (in_array($filename, $blacklist)); + } + + /** + * @brief check if the directory should be ignored when scanning + * NOTE: the special directories . and .. would cause never ending recursion + * @param String $dir + * @return boolean + */ + static public function isIgnoredDir($dir) { + if ($dir === '.' || $dir === '..') { + return true; + } + return false; + } + + /** + * following functions are equivalent to their php builtin equivalents for arguments/return values. + */ + static public function mkdir($path) { + return self::$defaultInstance->mkdir($path); + } + + static public function rmdir($path) { + return self::$defaultInstance->rmdir($path); + } + + static public function opendir($path) { + return self::$defaultInstance->opendir($path); + } + + static public function readdir($path) { + return self::$defaultInstance->readdir($path); + } + + static public function is_dir($path) { + return self::$defaultInstance->is_dir($path); + } + + static public function is_file($path) { + return self::$defaultInstance->is_file($path); + } + + static public function stat($path) { + return self::$defaultInstance->stat($path); + } + + static public function filetype($path) { + return self::$defaultInstance->filetype($path); + } + + static public function filesize($path) { + return self::$defaultInstance->filesize($path); + } + + static public function readfile($path) { + return self::$defaultInstance->readfile($path); + } + + static public function isCreatable($path) { + return self::$defaultInstance->isCreatable($path); + } + + static public function isReadable($path) { + return self::$defaultInstance->isReadable($path); + } + + static public function isUpdatable($path) { + return self::$defaultInstance->isUpdatable($path); + } + + static public function isDeletable($path) { + return self::$defaultInstance->isDeletable($path); + } + + static public function isSharable($path) { + return self::$defaultInstance->isSharable($path); + } + + static public function file_exists($path) { + return self::$defaultInstance->file_exists($path); + } + + static public function filemtime($path) { + return self::$defaultInstance->filemtime($path); + } + + static public function touch($path, $mtime = null) { + return self::$defaultInstance->touch($path, $mtime); + } + + static public function file_get_contents($path) { + return self::$defaultInstance->file_get_contents($path); + } + + static public function file_put_contents($path, $data) { + return self::$defaultInstance->file_put_contents($path, $data); + } + + static public function unlink($path) { + return self::$defaultInstance->unlink($path); + } + + static public function rename($path1, $path2) { + return self::$defaultInstance->rename($path1, $path2); + } + + static public function copy($path1, $path2) { + return self::$defaultInstance->copy($path1, $path2); + } + + static public function fopen($path, $mode) { + return self::$defaultInstance->fopen($path, $mode); + } + + static public function toTmpFile($path) { + return self::$defaultInstance->toTmpFile($path); + } + + static public function fromTmpFile($tmpFile, $path) { + return self::$defaultInstance->fromTmpFile($tmpFile, $path); + } + + static public function getMimeType($path) { + return self::$defaultInstance->getMimeType($path); + } + + static public function hash($type, $path, $raw = false) { + return self::$defaultInstance->hash($type, $path, $raw); + } + + static public function free_space($path = '/') { + return self::$defaultInstance->free_space($path); + } + + static public function search($query) { + return self::$defaultInstance->search($query); + } + + static public function searchByMime($query) { + return self::$defaultInstance->searchByMime($query); + } + + /** + * check if a file or folder has been updated since $time + * + * @param string $path + * @param int $time + * @return bool + */ + static public function hasUpdated($path, $time) { + return self::$defaultInstance->hasUpdated($path, $time); + } + + /** + * @brief Fix common problems with a file path + * @param string $path + * @param bool $stripTrailingSlash + * @return string + */ + public static function normalizePath($path, $stripTrailingSlash = true) { + if ($path == '') { + return '/'; + } + //no windows style slashes + $path = str_replace('\\', '/', $path); + + //add leading slash + if ($path[0] !== '/') { + $path = '/' . $path; + } + + // remove '/./' + // ugly, but str_replace() can't replace them all in one go + // as the replacement itself is part of the search string + // which will only be found during the next iteration + while (strpos($path, '/./') !== false) { + $path = str_replace('/./', '/', $path); + } + // remove sequences of slashes + $path = preg_replace('#/{2,}#', '/', $path); + + //remove trailing slash + if ($stripTrailingSlash and strlen($path) > 1 and substr($path, -1, 1) === '/') { + $path = substr($path, 0, -1); + } + + // remove trailing '/.' + if (substr($path, -2) == '/.') { + $path = substr($path, 0, -2); + } + + //normalize unicode if possible + $path = \OC_Util::normalizeUnicode($path); + + return $path; + } + + /** + * get the filesystem info + * + * @param string $path + * @param boolean $includeMountPoints whether to add mountpoint sizes, + * defaults to true + * @return array + * + * returns an associative array with the following keys: + * - size + * - mtime + * - mimetype + * - encrypted + * - versioned + */ + public static function getFileInfo($path, $includeMountPoints = true) { + return self::$defaultInstance->getFileInfo($path, $includeMountPoints); + } + + /** + * change file metadata + * + * @param string $path + * @param array $data + * @return int + * + * returns the fileid of the updated file + */ + public static function putFileInfo($path, $data) { + return self::$defaultInstance->putFileInfo($path, $data); + } + + /** + * get the content of a directory + * + * @param string $directory path under datadirectory + * @param string $mimetype_filter limit returned content to this mimetype or mimepart + * @return array + */ + public static function getDirectoryContent($directory, $mimetype_filter = '') { + return self::$defaultInstance->getDirectoryContent($directory, $mimetype_filter); + } + + /** + * Get the path of a file by id + * + * Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file + * + * @param int $id + * @return string + */ + public static function getPath($id) { + return self::$defaultInstance->getPath($id); + } + + /** + * Get the owner for a file or folder + * + * @param string $path + * @return string + */ + public static function getOwner($path) { + return self::$defaultInstance->getOwner($path); + } + + /** + * get the ETag for a file or folder + * + * @param string $path + * @return string + */ + static public function getETag($path) { + return self::$defaultInstance->getETag($path); + } +} + +\OC_Util::setupFS(); diff --git a/lib/private/files/mapper.php b/lib/private/files/mapper.php new file mode 100644 index 00000000000..47abd4e52fe --- /dev/null +++ b/lib/private/files/mapper.php @@ -0,0 +1,239 @@ +<?php + +namespace OC\Files; + +/** + * class Mapper is responsible to translate logical paths to physical paths and reverse + */ +class Mapper +{ + private $unchangedPhysicalRoot; + + public function __construct($rootDir) { + $this->unchangedPhysicalRoot = $rootDir; + } + + /** + * @param string $logicPath + * @param bool $create indicates if the generated physical name shall be stored in the database or not + * @return string the physical path + */ + public function logicToPhysical($logicPath, $create) { + $physicalPath = $this->resolveLogicPath($logicPath); + if ($physicalPath !== null) { + return $physicalPath; + } + + return $this->create($logicPath, $create); + } + + /** + * @param string $physicalPath + * @return string + */ + public function physicalToLogic($physicalPath) { + $logicPath = $this->resolvePhysicalPath($physicalPath); + if ($logicPath !== null) { + return $logicPath; + } + + $this->insert($physicalPath, $physicalPath); + return $physicalPath; + } + + /** + * @param string $path + * @param bool $isLogicPath indicates if $path is logical or physical + * @param $recursive + * @return void + */ + public function removePath($path, $isLogicPath, $recursive) { + if ($recursive) { + $path=$path.'%'; + } + + if ($isLogicPath) { + \OC_DB::executeAudited('DELETE FROM `*PREFIX*file_map` WHERE `logic_path` LIKE ?', array($path)); + } else { + \OC_DB::executeAudited('DELETE FROM `*PREFIX*file_map` WHERE `physic_path` LIKE ?', array($path)); + } + } + + /** + * @param $path1 + * @param $path2 + * @throws \Exception + */ + public function copy($path1, $path2) + { + $path1 = $this->stripLast($path1); + $path2 = $this->stripLast($path2); + $physicPath1 = $this->logicToPhysical($path1, true); + $physicPath2 = $this->logicToPhysical($path2, true); + + $sql = 'SELECT * FROM `*PREFIX*file_map` WHERE `logic_path` LIKE ?'; + $result = \OC_DB::executeAudited($sql, array($path1.'%')); + $updateQuery = \OC_DB::prepare('UPDATE `*PREFIX*file_map`' + .' SET `logic_path` = ?' + .' , `logic_path_hash` = ?' + .' , `physic_path` = ?' + .' , `physic_path_hash` = ?' + .' WHERE `logic_path` = ?'); + while( $row = $result->fetchRow()) { + $currentLogic = $row['logic_path']; + $currentPhysic = $row['physic_path']; + $newLogic = $path2.$this->stripRootFolder($currentLogic, $path1); + $newPhysic = $physicPath2.$this->stripRootFolder($currentPhysic, $physicPath1); + if ($path1 !== $currentLogic) { + try { + \OC_DB::executeAudited($updateQuery, array($newLogic, md5($newLogic), $newPhysic, md5($newPhysic), + $currentLogic)); + } catch (\Exception $e) { + error_log('Mapper::Copy failed '.$currentLogic.' -> '.$newLogic.'\n'.$e); + throw $e; + } + } + } + } + + /** + * @param $path + * @param $root + * @return bool|string + */ + public function stripRootFolder($path, $root) { + if (strpos($path, $root) !== 0) { + // throw exception ??? + return false; + } + if (strlen($path) > strlen($root)) { + return substr($path, strlen($root)); + } + + return ''; + } + + private function stripLast($path) { + if (substr($path, -1) == '/') { + $path = substr_replace($path, '', -1); + } + return $path; + } + + private function resolveLogicPath($logicPath) { + $logicPath = $this->stripLast($logicPath); + $sql = 'SELECT * FROM `*PREFIX*file_map` WHERE `logic_path_hash` = ?'; + $result = \OC_DB::executeAudited($sql, array(md5($logicPath))); + $result = $result->fetchRow(); + if ($result === false) { + return null; + } + + return $result['physic_path']; + } + + private function resolvePhysicalPath($physicalPath) { + $physicalPath = $this->stripLast($physicalPath); + $sql = \OC_DB::prepare('SELECT * FROM `*PREFIX*file_map` WHERE `physic_path_hash` = ?'); + $result = \OC_DB::executeAudited($sql, array(md5($physicalPath))); + $result = $result->fetchRow(); + + return $result['logic_path']; + } + + private function create($logicPath, $store) { + $logicPath = $this->stripLast($logicPath); + $index = 0; + + // create the slugified path + $physicalPath = $this->slugifyPath($logicPath); + + // detect duplicates + while ($this->resolvePhysicalPath($physicalPath) !== null) { + $physicalPath = $this->slugifyPath($logicPath, $index++); + } + + // insert the new path mapping if requested + if ($store) { + $this->insert($logicPath, $physicalPath); + } + + return $physicalPath; + } + + private function insert($logicPath, $physicalPath) { + $sql = 'INSERT INTO `*PREFIX*file_map` (`logic_path`, `physic_path`, `logic_path_hash`, `physic_path_hash`) + VALUES (?, ?, ?, ?)'; + \OC_DB::executeAudited($sql, array($logicPath, $physicalPath, md5($logicPath), md5($physicalPath))); + } + + public function slugifyPath($path, $index=null) { + $path = $this->stripRootFolder($path, $this->unchangedPhysicalRoot); + + $pathElements = explode('/', $path); + $sluggedElements = array(); + + $last= end($pathElements); + + foreach ($pathElements as $pathElement) { + // remove empty elements + if (empty($pathElement)) { + continue; + } + + $sluggedElements[] = self::slugify($pathElement); + } + + // apply index to file name + if ($index !== null) { + $last= array_pop($sluggedElements); + + // if filename contains periods - add index number before last period + if (preg_match('~\.[^\.]+$~i',$last,$extension)){ + array_push($sluggedElements, substr($last,0,-(strlen($extension[0]))).'-'.$index.$extension[0]); + } else { + // if filename doesn't contain periods add index ofter the last char + array_push($sluggedElements, $last.'-'.$index); + } + + } + + $sluggedPath = $this->unchangedPhysicalRoot.implode('/', $sluggedElements); + return $this->stripLast($sluggedPath); + } + + /** + * Modifies a string to remove all non ASCII characters and spaces. + * + * @param string $text + * @return string + */ + private function slugify($text) + { + // replace non letter or digits or dots by - + $text = preg_replace('~[^\\pL\d\.]+~u', '-', $text); + + // trim + $text = trim($text, '-'); + + // transliterate + if (function_exists('iconv')) { + $text = iconv('utf-8', 'us-ascii//TRANSLIT//IGNORE', $text); + } + + // lowercase + $text = strtolower($text); + + // remove unwanted characters + $text = preg_replace('~[^-\w\.]+~', '', $text); + + // trim ending dots (for security reasons and win compatibility) + $text = preg_replace('~\.+$~', '', $text); + + if (empty($text)) { + return uniqid(); + } + + return $text; + } +} diff --git a/lib/private/files/mount/manager.php b/lib/private/files/mount/manager.php new file mode 100644 index 00000000000..4c432dcf724 --- /dev/null +++ b/lib/private/files/mount/manager.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Mount; + +use \OC\Files\Filesystem; + +class Manager { + /** + * @var Mount[] + */ + private $mounts = array(); + + /** + * @param Mount $mount + */ + public function addMount($mount) { + $this->mounts[$mount->getMountPoint()] = $mount; + } + + /** + * Find the mount for $path + * + * @param $path + * @return Mount + */ + public function find($path) { + \OC_Util::setupFS(); + $path = $this->formatPath($path); + if (isset($this->mounts[$path])) { + return $this->mounts[$path]; + } + + \OC_Hook::emit('OC_Filesystem', 'get_mountpoint', array('path' => $path)); + $foundMountPoint = ''; + $mountPoints = array_keys($this->mounts); + foreach ($mountPoints as $mountpoint) { + if (strpos($path, $mountpoint) === 0 and strlen($mountpoint) > strlen($foundMountPoint)) { + $foundMountPoint = $mountpoint; + } + } + if (isset($this->mounts[$foundMountPoint])) { + return $this->mounts[$foundMountPoint]; + } else { + return null; + } + } + + /** + * Find all mounts in $path + * + * @param $path + * @return Mount[] + */ + public function findIn($path) { + \OC_Util::setupFS(); + $path = $this->formatPath($path); + $result = array(); + $pathLength = strlen($path); + $mountPoints = array_keys($this->mounts); + foreach ($mountPoints as $mountPoint) { + if (substr($mountPoint, 0, $pathLength) === $path and strlen($mountPoint) > $pathLength) { + $result[] = $this->mounts[$mountPoint]; + } + } + return $result; + } + + public function clear() { + $this->mounts = array(); + } + + /** + * Find mounts by storage id + * + * @param string $id + * @return Mount[] + */ + public function findByStorageId($id) { + \OC_Util::setupFS(); + if (strlen($id) > 64) { + $id = md5($id); + } + $result = array(); + foreach ($this->mounts as $mount) { + if ($mount->getStorageId() === $id) { + $result[] = $mount; + } + } + return $result; + } + + /** + * @return Mount[] + */ + public function getAll() { + return $this->mounts; + } + + /** + * Find mounts by numeric storage id + * + * @param string $id + * @return Mount + */ + public function findByNumericId($id) { + $storageId = \OC\Files\Cache\Storage::getStorageId($id); + return $this->findByStorageId($storageId); + } + + /** + * @param string $path + * @return string + */ + private function formatPath($path) { + $path = Filesystem::normalizePath($path); + if (strlen($path) > 1) { + $path .= '/'; + } + return $path; + } +} diff --git a/lib/private/files/mount/mount.php b/lib/private/files/mount/mount.php new file mode 100644 index 00000000000..0ce2f5975c7 --- /dev/null +++ b/lib/private/files/mount/mount.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Mount; + +use \OC\Files\Filesystem; +use OC\Files\Storage\Loader; +use OC\Files\Storage\Storage; + +class Mount { + /** + * @var \OC\Files\Storage\Storage $storage + */ + private $storage = null; + private $class; + private $storageId; + private $arguments = array(); + private $mountPoint; + + /** + * @var \OC\Files\Storage\Loader $loader + */ + private $loader; + + /** + * @param string | \OC\Files\Storage\Storage $storage + * @param string $mountpoint + * @param array $arguments (optional)\ + * @param \OC\Files\Storage\Loader $loader + */ + public function __construct($storage, $mountpoint, $arguments = null, $loader = null) { + if (is_null($arguments)) { + $arguments = array(); + } + if (is_null($loader)) { + $this->loader = new Loader(); + } else { + $this->loader = $loader; + } + + $mountpoint = $this->formatPath($mountpoint); + if ($storage instanceof Storage) { + $this->class = get_class($storage); + $this->storage = $this->loader->wrap($mountpoint, $storage); + } else { + // Update old classes to new namespace + if (strpos($storage, 'OC_Filestorage_') !== false) { + $storage = '\OC\Files\Storage\\' . substr($storage, 15); + } + $this->class = $storage; + $this->arguments = $arguments; + } + $this->mountPoint = $mountpoint; + } + + /** + * @return string + */ + public function getMountPoint() { + return $this->mountPoint; + } + + /** + * create the storage that is mounted + * + * @return \OC\Files\Storage\Storage + */ + private function createStorage() { + if (class_exists($this->class)) { + try { + return $this->loader->load($this->mountPoint, $this->class, $this->arguments); + } catch (\Exception $exception) { + \OC_Log::write('core', $exception->getMessage(), \OC_Log::ERROR); + return null; + } + } else { + \OC_Log::write('core', 'storage backend ' . $this->class . ' not found', \OC_Log::ERROR); + return null; + } + } + + /** + * @return \OC\Files\Storage\Storage + */ + public function getStorage() { + if (is_null($this->storage)) { + $this->storage = $this->createStorage(); + } + return $this->storage; + } + + /** + * @return string + */ + public function getStorageId() { + if (!$this->storageId) { + if (is_null($this->storage)) { + $storage = $this->createStorage(); //FIXME: start using exceptions + if (is_null($storage)) { + return null; + } + $this->storage = $storage; + } + $this->storageId = $this->storage->getId(); + if (strlen($this->storageId) > 64) { + $this->storageId = md5($this->storageId); + } + } + return $this->storageId; + } + + /** + * @param string $path + * @return string + */ + public function getInternalPath($path) { + if ($this->mountPoint === $path or $this->mountPoint . '/' === $path) { + $internalPath = ''; + } else { + $internalPath = substr($path, strlen($this->mountPoint)); + } + return $internalPath; + } + + /** + * @param string $path + * @return string + */ + private function formatPath($path) { + $path = Filesystem::normalizePath($path); + if (strlen($path) > 1) { + $path .= '/'; + } + return $path; + } + + /** + * @param callable $wrapper + */ + public function wrapStorage($wrapper) { + $this->storage = $wrapper($this->mountPoint, $this->storage); + } +} diff --git a/lib/private/files/node/file.php b/lib/private/files/node/file.php new file mode 100644 index 00000000000..75d5e0166b6 --- /dev/null +++ b/lib/private/files/node/file.php @@ -0,0 +1,155 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Node; + +use OCP\Files\NotPermittedException; + +class File extends Node implements \OCP\Files\File { + /** + * @return string + * @throws \OCP\Files\NotPermittedException + */ + public function getContent() { + if ($this->checkPermissions(\OCP\PERMISSION_READ)) { + /** + * @var \OC\Files\Storage\Storage $storage; + */ + return $this->view->file_get_contents($this->path); + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $data + * @throws \OCP\Files\NotPermittedException + */ + public function putContent($data) { + if ($this->checkPermissions(\OCP\PERMISSION_UPDATE)) { + $this->sendHooks(array('preWrite')); + $this->view->file_put_contents($this->path, $data); + $this->sendHooks(array('postWrite')); + } else { + throw new NotPermittedException(); + } + } + + /** + * @return string + */ + public function getMimeType() { + return $this->view->getMimeType($this->path); + } + + /** + * @param string $mode + * @return resource + * @throws \OCP\Files\NotPermittedException + */ + public function fopen($mode) { + $preHooks = array(); + $postHooks = array(); + $requiredPermissions = \OCP\PERMISSION_READ; + switch ($mode) { + case 'r+': + case 'rb+': + case 'w+': + case 'wb+': + case 'x+': + case 'xb+': + case 'a+': + case 'ab+': + case 'w': + case 'wb': + case 'x': + case 'xb': + case 'a': + case 'ab': + $preHooks[] = 'preWrite'; + $postHooks[] = 'postWrite'; + $requiredPermissions |= \OCP\PERMISSION_UPDATE; + break; + } + + if ($this->checkPermissions($requiredPermissions)) { + $this->sendHooks($preHooks); + $result = $this->view->fopen($this->path, $mode); + $this->sendHooks($postHooks); + return $result; + } else { + throw new NotPermittedException(); + } + } + + public function delete() { + if ($this->checkPermissions(\OCP\PERMISSION_DELETE)) { + $this->sendHooks(array('preDelete')); + $this->view->unlink($this->path); + $nonExisting = new NonExistingFile($this->root, $this->view, $this->path); + $this->root->emit('\OC\Files', 'postDelete', array($nonExisting)); + $this->exists = false; + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException + * @return \OC\Files\Node\Node + */ + public function copy($targetPath) { + $targetPath = $this->normalizePath($targetPath); + $parent = $this->root->get(dirname($targetPath)); + if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { + $nonExisting = new NonExistingFile($this->root, $this->view, $targetPath); + $this->root->emit('\OC\Files', 'preCopy', array($this, $nonExisting)); + $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); + $this->view->copy($this->path, $targetPath); + $targetNode = $this->root->get($targetPath); + $this->root->emit('\OC\Files', 'postCopy', array($this, $targetNode)); + $this->root->emit('\OC\Files', 'postWrite', array($targetNode)); + return $targetNode; + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException + * @return \OC\Files\Node\Node + */ + public function move($targetPath) { + $targetPath = $this->normalizePath($targetPath); + $parent = $this->root->get(dirname($targetPath)); + if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { + $nonExisting = new NonExistingFile($this->root, $this->view, $targetPath); + $this->root->emit('\OC\Files', 'preRename', array($this, $nonExisting)); + $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); + $this->view->rename($this->path, $targetPath); + $targetNode = $this->root->get($targetPath); + $this->root->emit('\OC\Files', 'postRename', array($this, $targetNode)); + $this->root->emit('\OC\Files', 'postWrite', array($targetNode)); + $this->path = $targetPath; + return $targetNode; + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $type + * @param bool $raw + * @return string + */ + public function hash($type, $raw = false) { + return $this->view->hash($type, $this->path, $raw); + } +} diff --git a/lib/private/files/node/folder.php b/lib/private/files/node/folder.php new file mode 100644 index 00000000000..923f53821b2 --- /dev/null +++ b/lib/private/files/node/folder.php @@ -0,0 +1,382 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Node; + +use OC\Files\Cache\Cache; +use OC\Files\Cache\Scanner; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; + +class Folder extends Node implements \OCP\Files\Folder { + /** + * @param string $path path relative to the folder + * @return string + * @throws \OCP\Files\NotPermittedException + */ + public function getFullPath($path) { + if (!$this->isValidPath($path)) { + throw new NotPermittedException(); + } + return $this->path . $this->normalizePath($path); + } + + /** + * @param string $path + * @throws \OCP\Files\NotFoundException + * @return string + */ + public function getRelativePath($path) { + if ($this->path === '' or $this->path === '/') { + return $this->normalizePath($path); + } + if (strpos($path, $this->path) !== 0) { + throw new NotFoundException(); + } else { + $path = substr($path, strlen($this->path)); + if (strlen($path) === 0) { + return '/'; + } else { + return $this->normalizePath($path); + } + } + } + + /** + * check if a node is a (grand-)child of the folder + * + * @param \OC\Files\Node\Node $node + * @return bool + */ + public function isSubNode($node) { + return strpos($node->getPath(), $this->path . '/') === 0; + } + + /** + * get the content of this directory + * + * @throws \OCP\Files\NotFoundException + * @return Node[] + */ + public function getDirectoryListing() { + $result = array(); + + /** + * @var \OC\Files\Storage\Storage $storage + */ + list($storage, $internalPath) = $this->view->resolvePath($this->path); + if ($storage) { + $cache = $storage->getCache($internalPath); + $permissionsCache = $storage->getPermissionsCache($internalPath); + + //trigger cache update check + $this->view->getFileInfo($this->path); + + $files = $cache->getFolderContents($internalPath); + $permissions = $permissionsCache->getDirectoryPermissions($this->getId(), $this->root->getUser()->getUID()); + } else { + $files = array(); + } + + //add a folder for any mountpoint in this directory and add the sizes of other mountpoints to the folders + $mounts = $this->root->getMountsIn($this->path); + $dirLength = strlen($this->path); + foreach ($mounts as $mount) { + $subStorage = $mount->getStorage(); + if ($subStorage) { + $subCache = $subStorage->getCache(''); + + if ($subCache->getStatus('') === Cache::NOT_FOUND) { + $subScanner = $subStorage->getScanner(''); + $subScanner->scanFile(''); + } + + $rootEntry = $subCache->get(''); + if ($rootEntry) { + $relativePath = trim(substr($mount->getMountPoint(), $dirLength), '/'); + if ($pos = strpos($relativePath, '/')) { + //mountpoint inside subfolder add size to the correct folder + $entryName = substr($relativePath, 0, $pos); + foreach ($files as &$entry) { + if ($entry['name'] === $entryName) { + if ($rootEntry['size'] >= 0) { + $entry['size'] += $rootEntry['size']; + } else { + $entry['size'] = -1; + } + } + } + } else { //mountpoint in this folder, add an entry for it + $rootEntry['name'] = $relativePath; + $rootEntry['storageObject'] = $subStorage; + + //remove any existing entry with the same name + foreach ($files as $i => $file) { + if ($file['name'] === $rootEntry['name']) { + $files[$i] = null; + break; + } + } + $files[] = $rootEntry; + } + } + } + } + + foreach ($files as $file) { + if ($file) { + if (isset($permissions[$file['fileid']])) { + $file['permissions'] = $permissions[$file['fileid']]; + } + $node = $this->createNode($this->path . '/' . $file['name'], $file); + $result[] = $node; + } + } + + return $result; + } + + /** + * @param string $path + * @param array $info + * @return File|Folder + */ + protected function createNode($path, $info = array()) { + if (!isset($info['mimetype'])) { + $isDir = $this->view->is_dir($path); + } else { + $isDir = $info['mimetype'] === 'httpd/unix-directory'; + } + if ($isDir) { + return new Folder($this->root, $this->view, $path); + } else { + return new File($this->root, $this->view, $path); + } + } + + /** + * Get the node at $path + * + * @param string $path + * @return \OC\Files\Node\Node + * @throws \OCP\Files\NotFoundException + */ + public function get($path) { + return $this->root->get($this->getFullPath($path)); + } + + /** + * @param string $path + * @return bool + */ + public function nodeExists($path) { + try { + $this->get($path); + return true; + } catch (NotFoundException $e) { + return false; + } + } + + /** + * @param string $path + * @return \OC\Files\Node\Folder + * @throws \OCP\Files\NotPermittedException + */ + public function newFolder($path) { + if ($this->checkPermissions(\OCP\PERMISSION_CREATE)) { + $fullPath = $this->getFullPath($path); + $nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath); + $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); + $this->root->emit('\OC\Files', 'preCreate', array($nonExisting)); + $this->view->mkdir($fullPath); + $node = new Folder($this->root, $this->view, $fullPath); + $this->root->emit('\OC\Files', 'postWrite', array($node)); + $this->root->emit('\OC\Files', 'postCreate', array($node)); + return $node; + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $path + * @return \OC\Files\Node\File + * @throws \OCP\Files\NotPermittedException + */ + public function newFile($path) { + if ($this->checkPermissions(\OCP\PERMISSION_CREATE)) { + $fullPath = $this->getFullPath($path); + $nonExisting = new NonExistingFile($this->root, $this->view, $fullPath); + $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); + $this->root->emit('\OC\Files', 'preCreate', array($nonExisting)); + $this->view->touch($fullPath); + $node = new File($this->root, $this->view, $fullPath); + $this->root->emit('\OC\Files', 'postWrite', array($node)); + $this->root->emit('\OC\Files', 'postCreate', array($node)); + return $node; + } else { + throw new NotPermittedException(); + } + } + + /** + * search for files with the name matching $query + * + * @param string $query + * @return \OC\Files\Node\Node[] + */ + public function search($query) { + return $this->searchCommon('%' . $query . '%', 'search'); + } + + /** + * search for files by mimetype + * + * @param string $mimetype + * @return Node[] + */ + public function searchByMime($mimetype) { + return $this->searchCommon($mimetype, 'searchByMime'); + } + + /** + * @param string $query + * @param string $method + * @return \OC\Files\Node\Node[] + */ + private function searchCommon($query, $method) { + $files = array(); + $rootLength = strlen($this->path); + /** + * @var \OC\Files\Storage\Storage $storage + */ + list($storage, $internalPath) = $this->view->resolvePath($this->path); + $internalRootLength = strlen($internalPath); + + $cache = $storage->getCache(''); + + $results = $cache->$method($query); + foreach ($results as $result) { + if ($internalRootLength === 0 or substr($result['path'], 0, $internalRootLength) === $internalPath) { + $result['internalPath'] = $result['path']; + $result['path'] = substr($result['path'], $internalRootLength); + $result['storage'] = $storage; + $files[] = $result; + } + } + + $mounts = $this->root->getMountsIn($this->path); + foreach ($mounts as $mount) { + $storage = $mount->getStorage(); + if ($storage) { + $cache = $storage->getCache(''); + + $relativeMountPoint = substr($mount->getMountPoint(), $rootLength); + $results = $cache->$method($query); + foreach ($results as $result) { + $result['internalPath'] = $result['path']; + $result['path'] = $relativeMountPoint . $result['path']; + $result['storage'] = $storage; + $files[] = $result; + } + } + } + + $result = array(); + foreach ($files as $file) { + $result[] = $this->createNode($this->normalizePath($this->path . '/' . $file['path']), $file); + } + + return $result; + } + + /** + * @param $id + * @return \OC\Files\Node\Node[] + */ + public function getById($id) { + $nodes = $this->root->getById($id); + $result = array(); + foreach ($nodes as $node) { + $pathPart = substr($node->getPath(), 0, strlen($this->getPath()) + 1); + if ($this->path === '/' or $pathPart === $this->getPath() . '/') { + $result[] = $node; + } + } + return $result; + } + + public function getFreeSpace() { + return $this->view->free_space($this->path); + } + + /** + * @return bool + */ + public function isCreatable() { + return $this->checkPermissions(\OCP\PERMISSION_CREATE); + } + + public function delete() { + if ($this->checkPermissions(\OCP\PERMISSION_DELETE)) { + $this->sendHooks(array('preDelete')); + $this->view->rmdir($this->path); + $nonExisting = new NonExistingFolder($this->root, $this->view, $this->path); + $this->root->emit('\OC\Files', 'postDelete', array($nonExisting)); + $this->exists = false; + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException + * @return \OC\Files\Node\Node + */ + public function copy($targetPath) { + $targetPath = $this->normalizePath($targetPath); + $parent = $this->root->get(dirname($targetPath)); + if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { + $nonExisting = new NonExistingFolder($this->root, $this->view, $targetPath); + $this->root->emit('\OC\Files', 'preCopy', array($this, $nonExisting)); + $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); + $this->view->copy($this->path, $targetPath); + $targetNode = $this->root->get($targetPath); + $this->root->emit('\OC\Files', 'postCopy', array($this, $targetNode)); + $this->root->emit('\OC\Files', 'postWrite', array($targetNode)); + return $targetNode; + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException + * @return \OC\Files\Node\Node + */ + public function move($targetPath) { + $targetPath = $this->normalizePath($targetPath); + $parent = $this->root->get(dirname($targetPath)); + if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { + $nonExisting = new NonExistingFolder($this->root, $this->view, $targetPath); + $this->root->emit('\OC\Files', 'preRename', array($this, $nonExisting)); + $this->root->emit('\OC\Files', 'preWrite', array($nonExisting)); + $this->view->rename($this->path, $targetPath); + $targetNode = $this->root->get($targetPath); + $this->root->emit('\OC\Files', 'postRename', array($this, $targetNode)); + $this->root->emit('\OC\Files', 'postWrite', array($targetNode)); + $this->path = $targetPath; + return $targetNode; + } else { + throw new NotPermittedException(); + } + } +} diff --git a/lib/private/files/node/node.php b/lib/private/files/node/node.php new file mode 100644 index 00000000000..063e2424a64 --- /dev/null +++ b/lib/private/files/node/node.php @@ -0,0 +1,245 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Node; + +use OC\Files\Cache\Cache; +use OC\Files\Cache\Scanner; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; + +class Node implements \OCP\Files\Node { + /** + * @var \OC\Files\View $view + */ + protected $view; + + /** + * @var \OC\Files\Node\Root $root + */ + protected $root; + + /** + * @var string $path + */ + protected $path; + + /** + * @param \OC\Files\View $view + * @param \OC\Files\Node\Root Root $root + * @param string $path + */ + public function __construct($root, $view, $path) { + $this->view = $view; + $this->root = $root; + $this->path = $path; + } + + /** + * @param string[] $hooks + */ + protected function sendHooks($hooks) { + foreach ($hooks as $hook) { + $this->root->emit('\OC\Files', $hook, array($this)); + } + } + + /** + * @param int $permissions + * @return bool + */ + protected function checkPermissions($permissions) { + return ($this->getPermissions() & $permissions) === $permissions; + } + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException + * @return \OC\Files\Node\Node + */ + public function move($targetPath) { + return; + } + + public function delete() { + return; + } + + /** + * @param string $targetPath + * @return \OC\Files\Node\Node + */ + public function copy($targetPath) { + return; + } + + /** + * @param int $mtime + * @throws \OCP\Files\NotPermittedException + */ + public function touch($mtime = null) { + if ($this->checkPermissions(\OCP\PERMISSION_UPDATE)) { + $this->sendHooks(array('preTouch')); + $this->view->touch($this->path, $mtime); + $this->sendHooks(array('postTouch')); + } else { + throw new NotPermittedException(); + } + } + + /** + * @return \OC\Files\Storage\Storage + * @throws \OCP\Files\NotFoundException + */ + public function getStorage() { + list($storage,) = $this->view->resolvePath($this->path); + return $storage; + } + + /** + * @return string + */ + public function getPath() { + return $this->path; + } + + /** + * @return string + */ + public function getInternalPath() { + list(, $internalPath) = $this->view->resolvePath($this->path); + return $internalPath; + } + + /** + * @return int + */ + public function getId() { + $info = $this->view->getFileInfo($this->path); + return $info['fileid']; + } + + /** + * @return array + */ + public function stat() { + return $this->view->stat($this->path); + } + + /** + * @return int + */ + public function getMTime() { + return $this->view->filemtime($this->path); + } + + /** + * @return int + */ + public function getSize() { + return $this->view->filesize($this->path); + } + + /** + * @return string + */ + public function getEtag() { + $info = $this->view->getFileInfo($this->path); + return $info['etag']; + } + + /** + * @return int + */ + public function getPermissions() { + $info = $this->view->getFileInfo($this->path); + return $info['permissions']; + } + + /** + * @return bool + */ + public function isReadable() { + return $this->checkPermissions(\OCP\PERMISSION_READ); + } + + /** + * @return bool + */ + public function isUpdateable() { + return $this->checkPermissions(\OCP\PERMISSION_UPDATE); + } + + /** + * @return bool + */ + public function isDeletable() { + return $this->checkPermissions(\OCP\PERMISSION_DELETE); + } + + /** + * @return bool + */ + public function isShareable() { + return $this->checkPermissions(\OCP\PERMISSION_SHARE); + } + + /** + * @return Node + */ + public function getParent() { + return $this->root->get(dirname($this->path)); + } + + /** + * @return string + */ + public function getName() { + return basename($this->path); + } + + /** + * @param string $path + * @return string + */ + protected function normalizePath($path) { + if ($path === '' or $path === '/') { + return '/'; + } + //no windows style slashes + $path = str_replace('\\', '/', $path); + //add leading slash + if ($path[0] !== '/') { + $path = '/' . $path; + } + //remove duplicate slashes + while (strpos($path, '//') !== false) { + $path = str_replace('//', '/', $path); + } + //remove trailing slash + $path = rtrim($path, '/'); + + return $path; + } + + /** + * check if the requested path is valid + * + * @param string $path + * @return bool + */ + public function isValidPath($path) { + if (!$path || $path[0] !== '/') { + $path = '/' . $path; + } + if (strstr($path, '/../') || strrchr($path, '/') === '/..') { + return false; + } + return true; + } +} diff --git a/lib/private/files/node/nonexistingfile.php b/lib/private/files/node/nonexistingfile.php new file mode 100644 index 00000000000..d45076f7fee --- /dev/null +++ b/lib/private/files/node/nonexistingfile.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Node; + +use OCP\Files\NotFoundException; + +class NonExistingFile extends File { + /** + * @param string $newPath + * @throws \OCP\Files\NotFoundException + */ + public function rename($newPath) { + throw new NotFoundException(); + } + + public function delete() { + throw new NotFoundException(); + } + + public function copy($newPath) { + throw new NotFoundException(); + } + + public function touch($mtime = null) { + throw new NotFoundException(); + } + + public function getId() { + throw new NotFoundException(); + } + + public function stat() { + throw new NotFoundException(); + } + + public function getMTime() { + throw new NotFoundException(); + } + + public function getSize() { + throw new NotFoundException(); + } + + public function getEtag() { + throw new NotFoundException(); + } + + public function getPermissions() { + throw new NotFoundException(); + } + + public function isReadable() { + throw new NotFoundException(); + } + + public function isUpdateable() { + throw new NotFoundException(); + } + + public function isDeletable() { + throw new NotFoundException(); + } + + public function isShareable() { + throw new NotFoundException(); + } + + public function getContent() { + throw new NotFoundException(); + } + + public function putContent($data) { + throw new NotFoundException(); + } + + public function getMimeType() { + throw new NotFoundException(); + } + + public function fopen($mode) { + throw new NotFoundException(); + } +} diff --git a/lib/private/files/node/nonexistingfolder.php b/lib/private/files/node/nonexistingfolder.php new file mode 100644 index 00000000000..0346cbf1e21 --- /dev/null +++ b/lib/private/files/node/nonexistingfolder.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Node; + +use OCP\Files\NotFoundException; + +class NonExistingFolder extends Folder { + /** + * @param string $newPath + * @throws \OCP\Files\NotFoundException + */ + public function rename($newPath) { + throw new NotFoundException(); + } + + public function delete() { + throw new NotFoundException(); + } + + public function copy($newPath) { + throw new NotFoundException(); + } + + public function touch($mtime = null) { + throw new NotFoundException(); + } + + public function getId() { + throw new NotFoundException(); + } + + public function stat() { + throw new NotFoundException(); + } + + public function getMTime() { + throw new NotFoundException(); + } + + public function getSize() { + throw new NotFoundException(); + } + + public function getEtag() { + throw new NotFoundException(); + } + + public function getPermissions() { + throw new NotFoundException(); + } + + public function isReadable() { + throw new NotFoundException(); + } + + public function isUpdateable() { + throw new NotFoundException(); + } + + public function isDeletable() { + throw new NotFoundException(); + } + + public function isShareable() { + throw new NotFoundException(); + } + + public function get($path) { + throw new NotFoundException(); + } + + public function getDirectoryListing() { + throw new NotFoundException(); + } + + public function nodeExists($path) { + return false; + } + + public function newFolder($path) { + throw new NotFoundException(); + } + + public function newFile($path) { + throw new NotFoundException(); + } + + public function search($pattern) { + throw new NotFoundException(); + } + + public function searchByMime($mime) { + throw new NotFoundException(); + } + + public function getById($id) { + throw new NotFoundException(); + } + + public function getFreeSpace() { + throw new NotFoundException(); + } + + public function isCreatable() { + throw new NotFoundException(); + } +} diff --git a/lib/private/files/node/root.php b/lib/private/files/node/root.php new file mode 100644 index 00000000000..e3d58476e9c --- /dev/null +++ b/lib/private/files/node/root.php @@ -0,0 +1,337 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Node; + +use OC\Files\Cache\Cache; +use OC\Files\Cache\Scanner; +use OC\Files\Mount\Manager; +use OC\Files\Mount\Mount; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OC\Hooks\Emitter; +use OC\Hooks\PublicEmitter; + +/** + * Class Root + * + * Hooks available in scope \OC\Files + * - preWrite(\OCP\Files\Node $node) + * - postWrite(\OCP\Files\Node $node) + * - preCreate(\OCP\Files\Node $node) + * - postCreate(\OCP\Files\Node $node) + * - preDelete(\OCP\Files\Node $node) + * - postDelete(\OCP\Files\Node $node) + * - preTouch(\OC\FilesP\Node $node, int $mtime) + * - postTouch(\OCP\Files\Node $node) + * - preCopy(\OCP\Files\Node $source, \OCP\Files\Node $target) + * - postCopy(\OCP\Files\Node $source, \OCP\Files\Node $target) + * - preRename(\OCP\Files\Node $source, \OCP\Files\Node $target) + * - postRename(\OCP\Files\Node $source, \OCP\Files\Node $target) + * + * @package OC\Files\Node + */ +class Root extends Folder implements Emitter { + + /** + * @var \OC\Files\Mount\Manager $mountManager + */ + private $mountManager; + + /** + * @var \OC\Hooks\PublicEmitter + */ + private $emitter; + + /** + * @var \OC\User\User $user + */ + private $user; + + /** + * @param \OC\Files\Mount\Manager $manager + * @param \OC\Files\View $view + * @param \OC\User\User $user + */ + public function __construct($manager, $view, $user) { + parent::__construct($this, $view, ''); + $this->mountManager = $manager; + $this->user = $user; + $this->emitter = new PublicEmitter(); + } + + /** + * Get the user for which the filesystem is setup + * + * @return \OC\User\User + */ + public function getUser() { + return $this->user; + } + + /** + * @param string $scope + * @param string $method + * @param callable $callback + */ + public function listen($scope, $method, $callback) { + $this->emitter->listen($scope, $method, $callback); + } + + /** + * @param string $scope optional + * @param string $method optional + * @param callable $callback optional + */ + public function removeListener($scope = null, $method = null, $callback = null) { + $this->emitter->removeListener($scope, $method, $callback); + } + + /** + * @param string $scope + * @param string $method + * @param array $arguments + */ + public function emit($scope, $method, $arguments = array()) { + $this->emitter->emit($scope, $method, $arguments); + } + + /** + * @param \OC\Files\Storage\Storage $storage + * @param string $mountPoint + * @param array $arguments + */ + public function mount($storage, $mountPoint, $arguments = array()) { + $mount = new Mount($storage, $mountPoint, $arguments); + $this->mountManager->addMount($mount); + } + + /** + * @param string $mountPoint + * @return \OC\Files\Mount\Mount + */ + public function getMount($mountPoint) { + return $this->mountManager->find($mountPoint); + } + + /** + * @param string $mountPoint + * @return \OC\Files\Mount\Mount[] + */ + public function getMountsIn($mountPoint) { + return $this->mountManager->findIn($mountPoint); + } + + /** + * @param string $storageId + * @return \OC\Files\Mount\Mount[] + */ + public function getMountByStorageId($storageId) { + return $this->mountManager->findByStorageId($storageId); + } + + /** + * @param int $numericId + * @return Mount[] + */ + public function getMountByNumericStorageId($numericId) { + return $this->mountManager->findByNumericId($numericId); + } + + /** + * @param \OC\Files\Mount\Mount $mount + */ + public function unMount($mount) { + $this->mountManager->remove($mount); + } + + /** + * @param string $path + * @throws \OCP\Files\NotFoundException + * @throws \OCP\Files\NotPermittedException + * @return Node + */ + public function get($path) { + $path = $this->normalizePath($path); + if ($this->isValidPath($path)) { + $fullPath = $this->getFullPath($path); + if ($this->view->file_exists($fullPath)) { + return $this->createNode($fullPath); + } else { + throw new NotFoundException(); + } + } else { + throw new NotPermittedException(); + } + } + + /** + * search file by id + * + * An array is returned because in the case where a single storage is mounted in different places the same file + * can exist in different places + * + * @param int $id + * @throws \OCP\Files\NotFoundException + * @return Node[] + */ + public function getById($id) { + $result = Cache::getById($id); + if (is_null($result)) { + throw new NotFoundException(); + } else { + list($storageId, $internalPath) = $result; + $nodes = array(); + $mounts = $this->mountManager->findByStorageId($storageId); + foreach ($mounts as $mount) { + $nodes[] = $this->get($mount->getMountPoint() . $internalPath); + } + return $nodes; + } + + } + + //most operations cant be done on the root + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException + * @return \OC\Files\Node\Node + */ + public function rename($targetPath) { + throw new NotPermittedException(); + } + + public function delete() { + throw new NotPermittedException(); + } + + /** + * @param string $targetPath + * @throws \OCP\Files\NotPermittedException + * @return \OC\Files\Node\Node + */ + public function copy($targetPath) { + throw new NotPermittedException(); + } + + /** + * @param int $mtime + * @throws \OCP\Files\NotPermittedException + */ + public function touch($mtime = null) { + throw new NotPermittedException(); + } + + /** + * @return \OC\Files\Storage\Storage + * @throws \OCP\Files\NotFoundException + */ + public function getStorage() { + throw new NotFoundException(); + } + + /** + * @return string + */ + public function getPath() { + return '/'; + } + + /** + * @return string + */ + public function getInternalPath() { + return ''; + } + + /** + * @return int + */ + public function getId() { + return null; + } + + /** + * @return array + */ + public function stat() { + return null; + } + + /** + * @return int + */ + public function getMTime() { + return null; + } + + /** + * @return int + */ + public function getSize() { + return null; + } + + /** + * @return string + */ + public function getEtag() { + return null; + } + + /** + * @return int + */ + public function getPermissions() { + return \OCP\PERMISSION_CREATE; + } + + /** + * @return bool + */ + public function isReadable() { + return false; + } + + /** + * @return bool + */ + public function isUpdateable() { + return false; + } + + /** + * @return bool + */ + public function isDeletable() { + return false; + } + + /** + * @return bool + */ + public function isShareable() { + return false; + } + + /** + * @return Node + * @throws \OCP\Files\NotFoundException + */ + public function getParent() { + throw new NotFoundException(); + } + + /** + * @return string + */ + public function getName() { + return ''; + } +} diff --git a/lib/private/files/storage/common.php b/lib/private/files/storage/common.php new file mode 100644 index 00000000000..678bf419023 --- /dev/null +++ b/lib/private/files/storage/common.php @@ -0,0 +1,373 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage; + +/** + * Storage backend class for providing common filesystem operation methods + * which are not storage-backend specific. + * + * \OC\Files\Storage\Common is never used directly; it is extended by all other + * storage backends, where its methods may be overridden, and additional + * (backend-specific) methods are defined. + * + * Some \OC\Files\Storage\Common methods call functions which are first defined + * in classes which extend it, e.g. $this->stat() . + */ + +abstract class Common implements \OC\Files\Storage\Storage { + protected $cache; + protected $scanner; + protected $permissioncache; + protected $watcher; + protected $storageCache; + + public function __construct($parameters) { + } + + public function is_dir($path) { + return $this->filetype($path) == 'dir'; + } + + public function is_file($path) { + return $this->filetype($path) == 'file'; + } + + public function filesize($path) { + if ($this->is_dir($path)) { + return 0; //by definition + } else { + $stat = $this->stat($path); + if (isset($stat['size'])) { + return $stat['size']; + } else { + return 0; + } + } + } + + public function isReadable($path) { + // at least check whether it exists + // subclasses might want to implement this more thoroughly + return $this->file_exists($path); + } + + public function isUpdatable($path) { + // at least check whether it exists + // subclasses might want to implement this more thoroughly + // a non-existing file/folder isn't updatable + return $this->file_exists($path); + } + + public function isCreatable($path) { + if ($this->is_dir($path) && $this->isUpdatable($path)) { + return true; + } + return false; + } + + public function isDeletable($path) { + return $this->isUpdatable($path); + } + + public function isSharable($path) { + return $this->isReadable($path); + } + + public function getPermissions($path) { + $permissions = 0; + if ($this->isCreatable($path)) { + $permissions |= \OCP\PERMISSION_CREATE; + } + if ($this->isReadable($path)) { + $permissions |= \OCP\PERMISSION_READ; + } + if ($this->isUpdatable($path)) { + $permissions |= \OCP\PERMISSION_UPDATE; + } + if ($this->isDeletable($path)) { + $permissions |= \OCP\PERMISSION_DELETE; + } + if ($this->isSharable($path)) { + $permissions |= \OCP\PERMISSION_SHARE; + } + return $permissions; + } + + public function filemtime($path) { + $stat = $this->stat($path); + if (isset($stat['mtime'])) { + return $stat['mtime']; + } else { + return 0; + } + } + + public function file_get_contents($path) { + $handle = $this->fopen($path, "r"); + if (!$handle) { + return false; + } + $size = $this->filesize($path); + if ($size == 0) { + return ''; + } + return fread($handle, $size); + } + + public function file_put_contents($path, $data) { + $handle = $this->fopen($path, "w"); + return fwrite($handle, $data); + } + + public function rename($path1, $path2) { + if ($this->copy($path1, $path2)) { + return $this->unlink($path1); + } else { + return false; + } + } + + public function copy($path1, $path2) { + $source = $this->fopen($path1, 'r'); + $target = $this->fopen($path2, 'w'); + list($count, $result) = \OC_Helper::streamCopy($source, $target); + return $result; + } + + /** + * @brief Deletes all files and folders recursively within a directory + * @param string $directory The directory whose contents will be deleted + * @param bool $empty Flag indicating whether directory will be emptied + * @returns bool + * + * @note By default the directory specified by $directory will be + * deleted together with its contents. To avoid this set $empty to true + */ + public function deleteAll($directory, $empty = false) { + $directory = trim($directory, '/'); + if (!$this->is_dir($directory) || !$this->isReadable($directory)) { + return false; + } else { + $directoryHandle = $this->opendir($directory); + if (is_resource($directoryHandle)) { + while (($contents = readdir($directoryHandle)) !== false) { + if (!\OC\Files\Filesystem::isIgnoredDir($contents)) { + $path = $directory . '/' . $contents; + if ($this->is_dir($path)) { + $this->deleteAll($path); + } else { + $this->unlink($path); + } + } + } + } + if ($empty === false) { + if (!$this->rmdir($directory)) { + return false; + } + } + return true; + } + + } + + public function getMimeType($path) { + if ($this->is_dir($path)) { + return 'httpd/unix-directory'; + } elseif ($this->file_exists($path)) { + return \OC_Helper::getFileNameMimeType($path); + } else { + return false; + } + } + + public function hash($type, $path, $raw = false) { + $tmpFile = $this->getLocalFile($path); + $hash = hash($type, $tmpFile, $raw); + unlink($tmpFile); + return $hash; + } + + public function search($query) { + return $this->searchInDir($query); + } + + public function getLocalFile($path) { + return $this->toTmpFile($path); + } + + private function toTmpFile($path) { //no longer in the storage api, still useful here + $source = $this->fopen($path, 'r'); + if (!$source) { + return false; + } + if ($pos = strrpos($path, '.')) { + $extension = substr($path, $pos); + } else { + $extension = ''; + } + $tmpFile = \OC_Helper::tmpFile($extension); + $target = fopen($tmpFile, 'w'); + \OC_Helper::streamCopy($source, $target); + return $tmpFile; + } + + public function getLocalFolder($path) { + $baseDir = \OC_Helper::tmpFolder(); + $this->addLocalFolder($path, $baseDir); + return $baseDir; + } + + private function addLocalFolder($path, $target) { + $dh = $this->opendir($path); + if (is_resource($dh)) { + while (($file = readdir($dh)) !== false) { + if ($file !== '.' and $file !== '..') { + if ($this->is_dir($path . '/' . $file)) { + mkdir($target . '/' . $file); + $this->addLocalFolder($path . '/' . $file, $target . '/' . $file); + } else { + $tmp = $this->toTmpFile($path . '/' . $file); + rename($tmp, $target . '/' . $file); + } + } + } + } + } + + protected function searchInDir($query, $dir = '') { + $files = array(); + $dh = $this->opendir($dir); + if (is_resource($dh)) { + while (($item = readdir($dh)) !== false) { + if ($item == '.' || $item == '..') continue; + if (strstr(strtolower($item), strtolower($query)) !== false) { + $files[] = $dir . '/' . $item; + } + if ($this->is_dir($dir . '/' . $item)) { + $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item)); + } + } + } + return $files; + } + + /** + * check if a file or folder has been updated since $time + * + * @param string $path + * @param int $time + * @return bool + */ + public function hasUpdated($path, $time) { + return $this->filemtime($path) > $time; + } + + public function getCache($path = '') { + if (!isset($this->cache)) { + $this->cache = new \OC\Files\Cache\Cache($this); + } + return $this->cache; + } + + public function getScanner($path = '') { + if (!isset($this->scanner)) { + $this->scanner = new \OC\Files\Cache\Scanner($this); + } + return $this->scanner; + } + + public function getPermissionsCache($path = '') { + if (!isset($this->permissioncache)) { + $this->permissioncache = new \OC\Files\Cache\Permissions($this); + } + return $this->permissioncache; + } + + public function getWatcher($path = '') { + if (!isset($this->watcher)) { + $this->watcher = new \OC\Files\Cache\Watcher($this); + } + return $this->watcher; + } + + public function getStorageCache() { + if (!isset($this->storageCache)) { + $this->storageCache = new \OC\Files\Cache\Storage($this); + } + return $this->storageCache; + } + + /** + * get the owner of a path + * + * @param string $path The path to get the owner + * @return string uid or false + */ + public function getOwner($path) { + return \OC_User::getUser(); + } + + /** + * get the ETag for a file or folder + * + * @param string $path + * @return string + */ + public function getETag($path) { + $ETagFunction = \OC_Connector_Sabre_Node::$ETagFunction; + if ($ETagFunction) { + $hash = call_user_func($ETagFunction, $path); + return $hash; + } else { + return uniqid(); + } + } + + /** + * clean a path, i.e. remove all redundant '.' and '..' + * making sure that it can't point to higher than '/' + * + * @param $path The path to clean + * @return string cleaned path + */ + public function cleanPath($path) { + if (strlen($path) == 0 or $path[0] != '/') { + $path = '/' . $path; + } + + $output = array(); + foreach (explode('/', $path) as $chunk) { + if ($chunk == '..') { + array_pop($output); + } else if ($chunk == '.') { + } else { + $output[] = $chunk; + } + } + return implode('/', $output); + } + + public function test() { + if ($this->stat('')) { + return true; + } + return false; + } + + /** + * get the free space in the storage + * + * @param $path + * @return int + */ + public function free_space($path) { + return \OC\Files\SPACE_UNKNOWN; + } +} diff --git a/lib/private/files/storage/commontest.php b/lib/private/files/storage/commontest.php new file mode 100644 index 00000000000..2394b14a82f --- /dev/null +++ b/lib/private/files/storage/commontest.php @@ -0,0 +1,80 @@ +<?php + +/** +* ownCloud +* +* @author Robin Appelman +* @copyright 2012 Robin Appelman icewind@owncloud.com +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see <http://www.gnu.org/licenses/>. +* +*/ + +/** + * test implementation for \OC\Files\Storage\Common with \OC\Files\Storage\Local + */ + +namespace OC\Files\Storage; + +class CommonTest extends \OC\Files\Storage\Common{ + /** + * underlying local storage used for missing functions + * @var \OC\Files\Storage\Local + */ + private $storage; + + public function __construct($params) { + $this->storage=new \OC\Files\Storage\Local($params); + } + + public function getId(){ + return 'test::'.$this->storage->getId(); + } + public function mkdir($path) { + return $this->storage->mkdir($path); + } + public function rmdir($path) { + return $this->storage->rmdir($path); + } + public function opendir($path) { + return $this->storage->opendir($path); + } + public function stat($path) { + return $this->storage->stat($path); + } + public function filetype($path) { + return @$this->storage->filetype($path); + } + public function isReadable($path) { + return $this->storage->isReadable($path); + } + public function isUpdatable($path) { + return $this->storage->isUpdatable($path); + } + public function file_exists($path) { + return $this->storage->file_exists($path); + } + public function unlink($path) { + return $this->storage->unlink($path); + } + public function fopen($path, $mode) { + return $this->storage->fopen($path, $mode); + } + public function free_space($path) { + return $this->storage->free_space($path); + } + public function touch($path, $mtime=null) { + return $this->storage->touch($path, $mtime); + } +} diff --git a/lib/private/files/storage/home.php b/lib/private/files/storage/home.php new file mode 100644 index 00000000000..1c2a682f197 --- /dev/null +++ b/lib/private/files/storage/home.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage; + +/** + * Specialized version of Local storage for home directory usage + */ +class Home extends Local { + /** + * @var string + */ + protected $id; + + /** + * @var \OC\User\User $user + */ + protected $user; + + /** + * @brief Construct a Home storage instance + * @param array $arguments array with "user" containing the + * storage owner and "legacy" containing "true" if the storage is + * a legacy storage with "local::" URL instead of the new "home::" one. + */ + public function __construct($arguments) { + $this->user = $arguments['user']; + $datadir = $this->user->getHome(); + if (isset($arguments['legacy']) && $arguments['legacy']) { + // legacy home id (<= 5.0.12) + $this->id = 'local::' . $datadir . '/'; + } + else { + $this->id = 'home::' . $this->user->getUID(); + } + + parent::__construct(array('datadir' => $datadir)); + } + + public function getId() { + return $this->id; + } + + /** + * @return \OC\Files\Cache\HomeCache + */ + public function getCache($path = '') { + if (!isset($this->cache)) { + $this->cache = new \OC\Files\Cache\HomeCache($this); + } + return $this->cache; + } + + /** + * @brief Returns the owner of this home storage + * @return \OC\User\User owner of this home storage + */ + public function getUser() { + return $this->user; + } +} diff --git a/lib/private/files/storage/loader.php b/lib/private/files/storage/loader.php new file mode 100644 index 00000000000..2572ef443bc --- /dev/null +++ b/lib/private/files/storage/loader.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage; + +class Loader { + /** + * @var callable[] $storageWrappers + */ + private $storageWrappers = array(); + + /** + * allow modifier storage behaviour by adding wrappers around storages + * + * $callback should be a function of type (string $mountPoint, Storage $storage) => Storage + * + * @param callable $callback + */ + public function addStorageWrapper($callback) { + $this->storageWrappers[] = $callback; + } + + public function load($mountPoint, $class, $arguments) { + return $this->wrap($mountPoint, new $class($arguments)); + } + + public function wrap($mountPoint, $storage) { + foreach ($this->storageWrappers as $wrapper) { + $storage = $wrapper($mountPoint, $storage); + } + return $storage; + } +} diff --git a/lib/private/files/storage/local.php b/lib/private/files/storage/local.php new file mode 100644 index 00000000000..02e8df4af4e --- /dev/null +++ b/lib/private/files/storage/local.php @@ -0,0 +1,302 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage; + +if (\OC_Util::runningOnWindows()) { + class Local extends MappedLocal { + + } +} else { + + /** + * for local filestore, we only have to map the paths + */ + class Local extends \OC\Files\Storage\Common { + protected $datadir; + + public function __construct($arguments) { + $this->datadir = $arguments['datadir']; + if (substr($this->datadir, -1) !== '/') { + $this->datadir .= '/'; + } + } + + public function __destruct() { + } + + public function getId() { + return 'local::' . $this->datadir; + } + + public function mkdir($path) { + return @mkdir($this->datadir . $path); + } + + public function rmdir($path) { + try { + $it = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->datadir . $path), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($it as $file) { + /** + * @var \SplFileInfo $file + */ + if (in_array($file->getBasename(), array('.', '..'))) { + continue; + } elseif ($file->isDir()) { + rmdir($file->getPathname()); + } elseif ($file->isFile() || $file->isLink()) { + unlink($file->getPathname()); + } + } + return rmdir($this->datadir . $path); + } catch (\UnexpectedValueException $e) { + return false; + } + } + + public function opendir($path) { + return opendir($this->datadir . $path); + } + + public function is_dir($path) { + if (substr($path, -1) == '/') { + $path = substr($path, 0, -1); + } + return is_dir($this->datadir . $path); + } + + public function is_file($path) { + return is_file($this->datadir . $path); + } + + public function stat($path) { + $fullPath = $this->datadir . $path; + $statResult = stat($fullPath); + + if ($statResult['size'] < 0) { + $size = self::getFileSizeFromOS($fullPath); + $statResult['size'] = $size; + $statResult[7] = $size; + } + return $statResult; + } + + public function filetype($path) { + $filetype = filetype($this->datadir . $path); + if ($filetype == 'link') { + $filetype = filetype(realpath($this->datadir . $path)); + } + return $filetype; + } + + public function filesize($path) { + if ($this->is_dir($path)) { + return 0; + } else { + $fullPath = $this->datadir . $path; + $fileSize = filesize($fullPath); + if ($fileSize < 0) { + return self::getFileSizeFromOS($fullPath); + } + + return $fileSize; + } + } + + public function isReadable($path) { + return is_readable($this->datadir . $path); + } + + public function isUpdatable($path) { + return is_writable($this->datadir . $path); + } + + public function file_exists($path) { + return file_exists($this->datadir . $path); + } + + public function filemtime($path) { + return filemtime($this->datadir . $path); + } + + public function touch($path, $mtime = null) { + // sets the modification time of the file to the given value. + // If mtime is nil the current time is set. + // note that the access time of the file always changes to the current time. + if ($this->file_exists($path) and !$this->isUpdatable($path)) { + return false; + } + if (!is_null($mtime)) { + $result = touch($this->datadir . $path, $mtime); + } else { + $result = touch($this->datadir . $path); + } + if ($result) { + clearstatcache(true, $this->datadir . $path); + } + + return $result; + } + + public function file_get_contents($path) { + return file_get_contents($this->datadir . $path); + } + + public function file_put_contents($path, $data) { //trigger_error("$path = ".var_export($path, 1)); + return file_put_contents($this->datadir . $path, $data); + } + + public function unlink($path) { + return $this->delTree($path); + } + + public function rename($path1, $path2) { + if (!$this->isUpdatable($path1)) { + \OC_Log::write('core', 'unable to rename, file is not writable : ' . $path1, \OC_Log::ERROR); + return false; + } + if (!$this->file_exists($path1)) { + \OC_Log::write('core', 'unable to rename, file does not exists : ' . $path1, \OC_Log::ERROR); + return false; + } + + if ($return = rename($this->datadir . $path1, $this->datadir . $path2)) { + } + return $return; + } + + public function copy($path1, $path2) { + if ($this->is_dir($path2)) { + if (!$this->file_exists($path2)) { + $this->mkdir($path2); + } + $source = substr($path1, strrpos($path1, '/') + 1); + $path2 .= $source; + } + return copy($this->datadir . $path1, $this->datadir . $path2); + } + + public function fopen($path, $mode) { + if ($return = fopen($this->datadir . $path, $mode)) { + switch ($mode) { + case 'r': + break; + case 'r+': + case 'w+': + case 'x+': + case 'a+': + break; + case 'w': + case 'x': + case 'a': + break; + } + } + return $return; + } + + private function delTree($dir) { + $dirRelative = $dir; + $dir = $this->datadir . $dir; + if (!file_exists($dir)) return true; + if (!is_dir($dir) || is_link($dir)) return unlink($dir); + foreach (scandir($dir) as $item) { + if ($item == '.' || $item == '..') continue; + if (is_file($dir . '/' . $item)) { + if (unlink($dir . '/' . $item)) { + } + } elseif (is_dir($dir . '/' . $item)) { + if (!$this->delTree($dirRelative . "/" . $item)) { + return false; + }; + } + } + if ($return = rmdir($dir)) { + } + return $return; + } + + private static function getFileSizeFromOS($fullPath) { + $name = strtolower(php_uname('s')); + // Windows OS: we use COM to access the filesystem + if (strpos($name, 'win') !== false) { + if (class_exists('COM')) { + $fsobj = new \COM("Scripting.FileSystemObject"); + $f = $fsobj->GetFile($fullPath); + return $f->Size; + } + } else if (strpos($name, 'bsd') !== false) { + if (\OC_Helper::is_function_enabled('exec')) { + return (float)exec('stat -f %z ' . escapeshellarg($fullPath)); + } + } else if (strpos($name, 'linux') !== false) { + if (\OC_Helper::is_function_enabled('exec')) { + return (float)exec('stat -c %s ' . escapeshellarg($fullPath)); + } + } else { + \OC_Log::write('core', + 'Unable to determine file size of "' . $fullPath . '". Unknown OS: ' . $name, + \OC_Log::ERROR); + } + + return 0; + } + + public function hash($path, $type, $raw = false) { + return hash_file($type, $this->datadir . $path, $raw); + } + + public function free_space($path) { + $space = @disk_free_space($this->datadir . $path); + if ($space === false) { + return \OC\Files\SPACE_UNKNOWN; + } + return $space; + } + + public function search($query) { + return $this->searchInDir($query); + } + + public function getLocalFile($path) { + return $this->datadir . $path; + } + + public function getLocalFolder($path) { + return $this->datadir . $path; + } + + protected function searchInDir($query, $dir = '') { + $files = array(); + foreach (scandir($this->datadir . $dir) as $item) { + if ($item == '.' || $item == '..') continue; + if (strstr(strtolower($item), strtolower($query)) !== false) { + $files[] = $dir . '/' . $item; + } + if (is_dir($this->datadir . $dir . '/' . $item)) { + $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item)); + } + } + return $files; + } + + /** + * check if a file or folder has been updated since $time + * + * @param string $path + * @param int $time + * @return bool + */ + public function hasUpdated($path, $time) { + return $this->filemtime($path) > $time; + } + } +} diff --git a/lib/private/files/storage/mappedlocal.php b/lib/private/files/storage/mappedlocal.php new file mode 100644 index 00000000000..6c37d445867 --- /dev/null +++ b/lib/private/files/storage/mappedlocal.php @@ -0,0 +1,357 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ +namespace OC\Files\Storage; + +/** + * for local filestore, we only have to map the paths + */ +class MappedLocal extends \OC\Files\Storage\Common{ + protected $datadir; + private $mapper; + + public function __construct($arguments) { + $this->datadir=$arguments['datadir']; + if(substr($this->datadir, -1)!=='/') { + $this->datadir.='/'; + } + + $this->mapper= new \OC\Files\Mapper($this->datadir); + } + public function __destruct() { + if (defined('PHPUNIT_RUN')) { + $this->mapper->removePath($this->datadir, true, true); + } + } + public function getId(){ + return 'local::'.$this->datadir; + } + public function mkdir($path) { + return @mkdir($this->buildPath($path)); + } + public function rmdir($path) { + try { + $it = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->buildPath($path)), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($it as $file) { + /** + * @var \SplFileInfo $file + */ + if (in_array($file->getBasename(), array('.', '..'))) { + continue; + } elseif ($file->isDir()) { + rmdir($file->getPathname()); + } elseif ($file->isFile() || $file->isLink()) { + unlink($file->getPathname()); + } + } + if ($result = @rmdir($this->buildPath($path))) { + $this->cleanMapper($path); + } + return $result; + } catch (\UnexpectedValueException $e) { + return false; + } + } + public function opendir($path) { + $files = array('.', '..'); + $physicalPath= $this->buildPath($path); + + $logicalPath = $this->mapper->physicalToLogic($physicalPath); + $dh = opendir($physicalPath); + if(is_resource($dh)) { + while (($file = readdir($dh)) !== false) { + if ($file === '.' or $file === '..') { + continue; + } + + $logicalFilePath = $this->mapper->physicalToLogic($physicalPath.'/'.$file); + + $file= $this->mapper->stripRootFolder($logicalFilePath, $logicalPath); + $file = $this->stripLeading($file); + $files[]= $file; + } + } + + \OC\Files\Stream\Dir::register('local-win32'.$path, $files); + return opendir('fakedir://local-win32'.$path); + } + public function is_dir($path) { + if(substr($path, -1)=='/') { + $path=substr($path, 0, -1); + } + return is_dir($this->buildPath($path)); + } + public function is_file($path) { + return is_file($this->buildPath($path)); + } + public function stat($path) { + $fullPath = $this->buildPath($path); + $statResult = stat($fullPath); + + if ($statResult['size'] < 0) { + $size = self::getFileSizeFromOS($fullPath); + $statResult['size'] = $size; + $statResult[7] = $size; + } + return $statResult; + } + public function filetype($path) { + $filetype=filetype($this->buildPath($path)); + if($filetype=='link') { + $filetype=filetype(realpath($this->buildPath($path))); + } + return $filetype; + } + public function filesize($path) { + if($this->is_dir($path)) { + return 0; + }else{ + $fullPath = $this->buildPath($path); + $fileSize = filesize($fullPath); + if ($fileSize < 0) { + return self::getFileSizeFromOS($fullPath); + } + + return $fileSize; + } + } + public function isReadable($path) { + return is_readable($this->buildPath($path)); + } + public function isUpdatable($path) { + return is_writable($this->buildPath($path)); + } + public function file_exists($path) { + return file_exists($this->buildPath($path)); + } + public function filemtime($path) { + return filemtime($this->buildPath($path)); + } + public function touch($path, $mtime=null) { + // sets the modification time of the file to the given value. + // If mtime is nil the current time is set. + // note that the access time of the file always changes to the current time. + if(!is_null($mtime)) { + $result=touch( $this->buildPath($path), $mtime ); + }else{ + $result=touch( $this->buildPath($path)); + } + if( $result ) { + clearstatcache( true, $this->buildPath($path) ); + } + + return $result; + } + public function file_get_contents($path) { + return file_get_contents($this->buildPath($path)); + } + public function file_put_contents($path, $data) { + return file_put_contents($this->buildPath($path), $data); + } + public function unlink($path) { + return $this->delTree($path); + } + public function rename($path1, $path2) { + if (!$this->isUpdatable($path1)) { + \OC_Log::write('core', 'unable to rename, file is not writable : '.$path1, \OC_Log::ERROR); + return false; + } + if(! $this->file_exists($path1)) { + \OC_Log::write('core', 'unable to rename, file does not exists : '.$path1, \OC_Log::ERROR); + return false; + } + + $physicPath1 = $this->buildPath($path1); + $physicPath2 = $this->buildPath($path2); + if($return=rename($physicPath1, $physicPath2)) { + // mapper needs to create copies or all children + $this->copyMapping($path1, $path2); + $this->cleanMapper($physicPath1, false, true); + } + return $return; + } + public function copy($path1, $path2) { + if($this->is_dir($path2)) { + if(!$this->file_exists($path2)) { + $this->mkdir($path2); + } + $source=substr($path1, strrpos($path1, '/')+1); + $path2.=$source; + } + if($return=copy($this->buildPath($path1), $this->buildPath($path2))) { + // mapper needs to create copies or all children + $this->copyMapping($path1, $path2); + } + return $return; + } + public function fopen($path, $mode) { + if($return=fopen($this->buildPath($path), $mode)) { + switch($mode) { + case 'r': + break; + case 'r+': + case 'w+': + case 'x+': + case 'a+': + break; + case 'w': + case 'x': + case 'a': + break; + } + } + return $return; + } + + private function delTree($dir, $isLogicPath=true) { + $dirRelative=$dir; + if ($isLogicPath) { + $dir=$this->buildPath($dir); + } + if (!file_exists($dir)) { + return true; + } + if (!is_dir($dir) || is_link($dir)) { + if($return=unlink($dir)) { + $this->cleanMapper($dir, false); + return $return; + } + } + foreach (scandir($dir) as $item) { + if ($item == '.' || $item == '..') { + continue; + } + if(is_file($dir.'/'.$item)) { + if(unlink($dir.'/'.$item)) { + $this->cleanMapper($dir.'/'.$item, false); + } + }elseif(is_dir($dir.'/'.$item)) { + if (!$this->delTree($dir. "/" . $item, false)) { + return false; + }; + } + } + if($return=rmdir($dir)) { + $this->cleanMapper($dir, false); + } + return $return; + } + + private static function getFileSizeFromOS($fullPath) { + $name = strtolower(php_uname('s')); + // Windows OS: we use COM to access the filesystem + if (strpos($name, 'win') !== false) { + if (class_exists('COM')) { + $fsobj = new \COM("Scripting.FileSystemObject"); + $f = $fsobj->GetFile($fullPath); + return $f->Size; + } + } else if (strpos($name, 'bsd') !== false) { + if (\OC_Helper::is_function_enabled('exec')) { + return (float)exec('stat -f %z ' . escapeshellarg($fullPath)); + } + } else if (strpos($name, 'linux') !== false) { + if (\OC_Helper::is_function_enabled('exec')) { + return (float)exec('stat -c %s ' . escapeshellarg($fullPath)); + } + } else { + \OC_Log::write('core', + 'Unable to determine file size of "'.$fullPath.'". Unknown OS: '.$name, + \OC_Log::ERROR); + } + + return 0; + } + + public function hash($path, $type, $raw=false) { + return hash_file($type, $this->buildPath($path), $raw); + } + + public function free_space($path) { + return @disk_free_space($this->buildPath($path)); + } + + public function search($query) { + return $this->searchInDir($query); + } + public function getLocalFile($path) { + return $this->buildPath($path); + } + public function getLocalFolder($path) { + return $this->buildPath($path); + } + + protected function searchInDir($query, $dir='') { + $files=array(); + $physicalDir = $this->buildPath($dir); + foreach (scandir($physicalDir) as $item) { + if ($item == '.' || $item == '..') + continue; + $physicalItem = $this->mapper->physicalToLogic($physicalDir.'/'.$item); + $item = substr($physicalItem, strlen($physicalDir)+1); + + if(strstr(strtolower($item), strtolower($query)) !== false) { + $files[]=$dir.'/'.$item; + } + if(is_dir($physicalItem)) { + $files=array_merge($files, $this->searchInDir($query, $dir.'/'.$item)); + } + } + return $files; + } + + /** + * check if a file or folder has been updated since $time + * @param string $path + * @param int $time + * @return bool + */ + public function hasUpdated($path, $time) { + return $this->filemtime($path)>$time; + } + + private function buildPath($path, $create=true) { + $path = $this->stripLeading($path); + $fullPath = $this->datadir.$path; + return $this->mapper->logicToPhysical($fullPath, $create); + } + + private function cleanMapper($path, $isLogicPath=true, $recursive=true) { + $fullPath = $path; + if ($isLogicPath) { + $fullPath = $this->datadir.$path; + } + $this->mapper->removePath($fullPath, $isLogicPath, $recursive); + } + + private function copyMapping($path1, $path2) { + $path1 = $this->stripLeading($path1); + $path2 = $this->stripLeading($path2); + + $fullPath1 = $this->datadir.$path1; + $fullPath2 = $this->datadir.$path2; + + $this->mapper->copy($fullPath1, $fullPath2); + } + + private function stripLeading($path) { + if(strpos($path, '/') === 0) { + $path = substr($path, 1); + } + if(strpos($path, '\\') === 0) { + $path = substr($path, 1); + } + if ($path === false) { + return ''; + } + + return $path; + } +} diff --git a/lib/private/files/storage/storage.php b/lib/private/files/storage/storage.php new file mode 100644 index 00000000000..5be90f24756 --- /dev/null +++ b/lib/private/files/storage/storage.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage; + +/** + * Provide a common interface to all different storage options + * + * All paths passed to the storage are relative to the storage and should NOT have a leading slash. + */ +interface Storage extends \OCP\Files\Storage { + + /** + * get a cache instance for the storage + * + * @param string $path + * @return \OC\Files\Cache\Cache + */ + public function getCache($path = ''); + + /** + * get a scanner instance for the storage + * + * @param string $path + * @return \OC\Files\Cache\Scanner + */ + public function getScanner($path = ''); + + + /** + * get the user id of the owner of a file or folder + * + * @param string $path + * @return string + */ + public function getOwner($path); + + /** + * get a permissions cache instance for the cache + * + * @param string $path + * @return \OC\Files\Cache\Permissions + */ + public function getPermissionsCache($path = ''); + + /** + * get a watcher instance for the cache + * + * @param string $path + * @return \OC\Files\Cache\Watcher + */ + public function getWatcher($path = ''); + + /** + * @return \OC\Files\Cache\Storage + */ + public function getStorageCache(); + +} diff --git a/lib/private/files/storage/temporary.php b/lib/private/files/storage/temporary.php new file mode 100644 index 00000000000..d84dbda2e39 --- /dev/null +++ b/lib/private/files/storage/temporary.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage; + +/** + * local storage backend in temporary folder for testing purpose + */ +class Temporary extends Local{ + public function __construct($arguments) { + parent::__construct(array('datadir' => \OC_Helper::tmpFolder())); + } + + public function cleanUp() { + \OC_Helper::rmdirr($this->datadir); + } + + public function __destruct() { + parent::__destruct(); + $this->cleanUp(); + } +} diff --git a/lib/private/files/storage/wrapper/quota.php b/lib/private/files/storage/wrapper/quota.php new file mode 100644 index 00000000000..43016e0892f --- /dev/null +++ b/lib/private/files/storage/wrapper/quota.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage\Wrapper; + +class Quota extends Wrapper { + + /** + * @var int $quota + */ + protected $quota; + + /** + * @param array $parameters + */ + public function __construct($parameters) { + $this->storage = $parameters['storage']; + $this->quota = $parameters['quota']; + } + + protected function getSize($path) { + $cache = $this->getCache(); + $data = $cache->get($path); + if (is_array($data) and isset($data['size'])) { + return $data['size']; + } else { + return \OC\Files\SPACE_NOT_COMPUTED; + } + } + + /** + * Get free space as limited by the quota + * + * @param string $path + * @return int + */ + public function free_space($path) { + if ($this->quota < 0) { + return $this->storage->free_space($path); + } else { + $used = $this->getSize(''); + if ($used < 0) { + return \OC\Files\SPACE_NOT_COMPUTED; + } else { + $free = $this->storage->free_space($path); + return min($free, (max($this->quota - $used, 0))); + } + } + } + + /** + * see http://php.net/manual/en/function.file_put_contents.php + * + * @param string $path + * @param string $data + * @return bool + */ + public function file_put_contents($path, $data) { + $free = $this->free_space(''); + if ($free < 0 or strlen($data) < $free) { + return $this->storage->file_put_contents($path, $data); + } else { + return false; + } + } + + /** + * see http://php.net/manual/en/function.copy.php + * + * @param string $source + * @param string $target + * @return bool + */ + public function copy($source, $target) { + $free = $this->free_space(''); + if ($free < 0 or $this->getSize($source) < $free) { + return $this->storage->copy($source, $target); + } else { + return false; + } + } + + /** + * see http://php.net/manual/en/function.fopen.php + * + * @param string $path + * @param string $mode + * @return resource + */ + public function fopen($path, $mode) { + $source = $this->storage->fopen($path, $mode); + $free = $this->free_space(''); + if ($free >= 0 && $mode !== 'r') { + return \OC\Files\Stream\Quota::wrap($source, $free); + } else { + return $source; + } + } +} diff --git a/lib/private/files/storage/wrapper/wrapper.php b/lib/private/files/storage/wrapper/wrapper.php new file mode 100644 index 00000000000..f9adda80314 --- /dev/null +++ b/lib/private/files/storage/wrapper/wrapper.php @@ -0,0 +1,435 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage\Wrapper; + +class Wrapper implements \OC\Files\Storage\Storage { + /** + * @var \OC\Files\Storage\Storage $storage + */ + protected $storage; + + /** + * @param array $parameters + */ + public function __construct($parameters) { + $this->storage = $parameters['storage']; + } + + /** + * @return \OC\Files\Storage\Storage + */ + public function getWrapperStorage() { + return $this->storage; + } + + /** + * Get the identifier for the storage, + * the returned id should be the same for every storage object that is created with the same parameters + * and two storage objects with the same id should refer to two storages that display the same files. + * + * @return string + */ + public function getId() { + return $this->storage->getId(); + } + + /** + * see http://php.net/manual/en/function.mkdir.php + * + * @param string $path + * @return bool + */ + public function mkdir($path) { + return $this->storage->mkdir($path); + } + + /** + * see http://php.net/manual/en/function.rmdir.php + * + * @param string $path + * @return bool + */ + public function rmdir($path) { + return $this->storage->rmdir($path); + } + + /** + * see http://php.net/manual/en/function.opendir.php + * + * @param string $path + * @return resource + */ + public function opendir($path) { + return $this->storage->opendir($path); + } + + /** + * see http://php.net/manual/en/function.is_dir.php + * + * @param string $path + * @return bool + */ + public function is_dir($path) { + return $this->storage->is_dir($path); + } + + /** + * see http://php.net/manual/en/function.is_file.php + * + * @param string $path + * @return bool + */ + public function is_file($path) { + return $this->storage->is_file($path); + } + + /** + * see http://php.net/manual/en/function.stat.php + * only the following keys are required in the result: size and mtime + * + * @param string $path + * @return array + */ + public function stat($path) { + return $this->storage->stat($path); + } + + /** + * see http://php.net/manual/en/function.filetype.php + * + * @param string $path + * @return bool + */ + public function filetype($path) { + return $this->storage->filetype($path); + } + + /** + * see http://php.net/manual/en/function.filesize.php + * The result for filesize when called on a folder is required to be 0 + * + * @param string $path + * @return int + */ + public function filesize($path) { + return $this->storage->filesize($path); + } + + /** + * check if a file can be created in $path + * + * @param string $path + * @return bool + */ + public function isCreatable($path) { + return $this->storage->isCreatable($path); + } + + /** + * check if a file can be read + * + * @param string $path + * @return bool + */ + public function isReadable($path) { + return $this->storage->isReadable($path); + } + + /** + * check if a file can be written to + * + * @param string $path + * @return bool + */ + public function isUpdatable($path) { + return $this->storage->isUpdatable($path); + } + + /** + * check if a file can be deleted + * + * @param string $path + * @return bool + */ + public function isDeletable($path) { + return $this->storage->isDeletable($path); + } + + /** + * check if a file can be shared + * + * @param string $path + * @return bool + */ + public function isSharable($path) { + return $this->storage->isSharable($path); + } + + /** + * get the full permissions of a path. + * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php + * + * @param string $path + * @return int + */ + public function getPermissions($path) { + return $this->storage->getPermissions($path); + } + + /** + * see http://php.net/manual/en/function.file_exists.php + * + * @param string $path + * @return bool + */ + public function file_exists($path) { + return $this->storage->file_exists($path); + } + + /** + * see http://php.net/manual/en/function.filemtime.php + * + * @param string $path + * @return int + */ + public function filemtime($path) { + return $this->storage->filemtime($path); + } + + /** + * see http://php.net/manual/en/function.file_get_contents.php + * + * @param string $path + * @return string + */ + public function file_get_contents($path) { + return $this->storage->file_get_contents($path); + } + + /** + * see http://php.net/manual/en/function.file_put_contents.php + * + * @param string $path + * @param string $data + * @return bool + */ + public function file_put_contents($path, $data) { + return $this->storage->file_put_contents($path, $data); + } + + /** + * see http://php.net/manual/en/function.unlink.php + * + * @param string $path + * @return bool + */ + public function unlink($path) { + return $this->storage->unlink($path); + } + + /** + * see http://php.net/manual/en/function.rename.php + * + * @param string $path1 + * @param string $path2 + * @return bool + */ + public function rename($path1, $path2) { + return $this->storage->rename($path1, $path2); + } + + /** + * see http://php.net/manual/en/function.copy.php + * + * @param string $path1 + * @param string $path2 + * @return bool + */ + public function copy($path1, $path2) { + return $this->storage->copy($path1, $path2); + } + + /** + * see http://php.net/manual/en/function.fopen.php + * + * @param string $path + * @param string $mode + * @return resource + */ + public function fopen($path, $mode) { + return $this->storage->fopen($path, $mode); + } + + /** + * get the mimetype for a file or folder + * The mimetype for a folder is required to be "httpd/unix-directory" + * + * @param string $path + * @return string + */ + public function getMimeType($path) { + return $this->storage->getMimeType($path); + } + + /** + * see http://php.net/manual/en/function.hash.php + * + * @param string $type + * @param string $path + * @param bool $raw + * @return string + */ + public function hash($type, $path, $raw = false) { + return $this->storage->hash($type, $path, $raw); + } + + /** + * see http://php.net/manual/en/function.free_space.php + * + * @param string $path + * @return int + */ + public function free_space($path) { + return $this->storage->free_space($path); + } + + /** + * search for occurrences of $query in file names + * + * @param string $query + * @return array + */ + public function search($query) { + return $this->storage->search($query); + } + + /** + * see http://php.net/manual/en/function.touch.php + * If the backend does not support the operation, false should be returned + * + * @param string $path + * @param int $mtime + * @return bool + */ + public function touch($path, $mtime = null) { + return $this->storage->touch($path, $mtime); + } + + /** + * get the path to a local version of the file. + * The local version of the file can be temporary and doesn't have to be persistent across requests + * + * @param string $path + * @return string + */ + public function getLocalFile($path) { + return $this->storage->getLocalFile($path); + } + + /** + * get the path to a local version of the folder. + * The local version of the folder can be temporary and doesn't have to be persistent across requests + * + * @param string $path + * @return string + */ + public function getLocalFolder($path) { + return $this->storage->getLocalFolder($path); + } + + /** + * check if a file or folder has been updated since $time + * + * @param string $path + * @param int $time + * @return bool + * + * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed. + * returning true for other changes in the folder is optional + */ + public function hasUpdated($path, $time) { + return $this->storage->hasUpdated($path, $time); + } + + /** + * get a cache instance for the storage + * + * @param string $path + * @return \OC\Files\Cache\Cache + */ + public function getCache($path = '') { + return $this->storage->getCache($path); + } + + /** + * get a scanner instance for the storage + * + * @param string $path + * @return \OC\Files\Cache\Scanner + */ + public function getScanner($path = '') { + return $this->storage->getScanner($path); + } + + + /** + * get the user id of the owner of a file or folder + * + * @param string $path + * @return string + */ + public function getOwner($path) { + return $this->storage->getOwner($path); + } + + /** + * get a permissions cache instance for the cache + * + * @param string $path + * @return \OC\Files\Cache\Permissions + */ + public function getPermissionsCache($path = '') { + return $this->storage->getPermissionsCache($path); + } + + /** + * get a watcher instance for the cache + * + * @param string $path + * @return \OC\Files\Cache\Watcher + */ + public function getWatcher($path = '') { + return $this->storage->getWatcher($path); + } + + /** + * @return \OC\Files\Cache\Storage + */ + public function getStorageCache() { + return $this->storage->getStorageCache(); + } + + /** + * get the ETag for a file or folder + * + * @param string $path + * @return string + */ + public function getETag($path) { + return $this->storage->getETag($path); + } + + /** + * Returns true + * @return true + */ + public function test() { + return $this->storage->test(); + } +} diff --git a/lib/private/files/stream/close.php b/lib/private/files/stream/close.php new file mode 100644 index 00000000000..80de3497c36 --- /dev/null +++ b/lib/private/files/stream/close.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Stream; + +/** + * stream wrapper that provides a callback on stream close + */ +class Close { + private static $callBacks = array(); + private $path = ''; + private $source; + private static $open = array(); + + public function stream_open($path, $mode, $options, &$opened_path) { + $path = substr($path, strlen('close://')); + $this->path = $path; + $this->source = fopen($path, $mode); + if (is_resource($this->source)) { + $this->meta = stream_get_meta_data($this->source); + } + self::$open[] = $path; + return is_resource($this->source); + } + + public function stream_seek($offset, $whence = SEEK_SET) { + fseek($this->source, $offset, $whence); + } + + public function stream_tell() { + return ftell($this->source); + } + + public function stream_read($count) { + return fread($this->source, $count); + } + + public function stream_write($data) { + return fwrite($this->source, $data); + } + + public function stream_set_option($option, $arg1, $arg2) { + switch ($option) { + case STREAM_OPTION_BLOCKING: + stream_set_blocking($this->source, $arg1); + break; + case STREAM_OPTION_READ_TIMEOUT: + stream_set_timeout($this->source, $arg1, $arg2); + break; + case STREAM_OPTION_WRITE_BUFFER: + stream_set_write_buffer($this->source, $arg1, $arg2); + } + } + + public function stream_stat() { + return fstat($this->source); + } + + public function stream_lock($mode) { + flock($this->source, $mode); + } + + public function stream_flush() { + return fflush($this->source); + } + + public function stream_eof() { + return feof($this->source); + } + + public function url_stat($path) { + $path = substr($path, strlen('close://')); + if (file_exists($path)) { + return stat($path); + } else { + return false; + } + } + + public function stream_close() { + fclose($this->source); + if (isset(self::$callBacks[$this->path])) { + call_user_func(self::$callBacks[$this->path], $this->path); + } + } + + public function unlink($path) { + $path = substr($path, strlen('close://')); + return unlink($path); + } + + public static function registerCallback($path, $callback) { + self::$callBacks[$path] = $callback; + } +} diff --git a/lib/private/files/stream/dir.php b/lib/private/files/stream/dir.php new file mode 100644 index 00000000000..6ca884fc994 --- /dev/null +++ b/lib/private/files/stream/dir.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Stream; + +class Dir { + private static $dirs = array(); + private $name; + private $index; + + public function dir_opendir($path, $options) { + $this->name = substr($path, strlen('fakedir://')); + $this->index = 0; + if (!isset(self::$dirs[$this->name])) { + self::$dirs[$this->name] = array(); + } + return true; + } + + public function dir_readdir() { + if ($this->index >= count(self::$dirs[$this->name])) { + return false; + } + $filename = self::$dirs[$this->name][$this->index]; + $this->index++; + return $filename; + } + + public function dir_closedir() { + $this->name = ''; + return true; + } + + public function dir_rewinddir() { + $this->index = 0; + return true; + } + + public static function register($path, $content) { + self::$dirs[$path] = $content; + } +} diff --git a/lib/private/files/stream/oc.php b/lib/private/files/stream/oc.php new file mode 100644 index 00000000000..88e7e062df9 --- /dev/null +++ b/lib/private/files/stream/oc.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Stream; + +/** + * a stream wrappers for ownCloud's virtual filesystem + */ +class OC { + /** + * @var \OC\Files\View + */ + static private $rootView; + + private $path; + private $dirSource; + private $fileSource; + private $meta; + + private function setup(){ + if (!self::$rootView) { + self::$rootView = new \OC\Files\View(''); + } + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $this->setup(); + $path = substr($path, strlen('oc://')); + $this->path = $path; + $this->fileSource = self::$rootView->fopen($path, $mode); + if (is_resource($this->fileSource)) { + $this->meta = stream_get_meta_data($this->fileSource); + } + return is_resource($this->fileSource); + } + + public function stream_seek($offset, $whence = SEEK_SET) { + fseek($this->fileSource, $offset, $whence); + } + + public function stream_tell() { + return ftell($this->fileSource); + } + + public function stream_read($count) { + return fread($this->fileSource, $count); + } + + public function stream_write($data) { + return fwrite($this->fileSource, $data); + } + + public function stream_set_option($option, $arg1, $arg2) { + switch ($option) { + case STREAM_OPTION_BLOCKING: + stream_set_blocking($this->fileSource, $arg1); + break; + case STREAM_OPTION_READ_TIMEOUT: + stream_set_timeout($this->fileSource, $arg1, $arg2); + break; + case STREAM_OPTION_WRITE_BUFFER: + stream_set_write_buffer($this->fileSource, $arg1, $arg2); + } + } + + public function stream_stat() { + return fstat($this->fileSource); + } + + public function stream_lock($mode) { + flock($this->fileSource, $mode); + } + + public function stream_flush() { + return fflush($this->fileSource); + } + + public function stream_eof() { + return feof($this->fileSource); + } + + public function url_stat($path) { + $this->setup(); + $path = substr($path, strlen('oc://')); + if (self::$rootView->file_exists($path)) { + return self::$rootView->stat($path); + } else { + return false; + } + } + + public function stream_close() { + fclose($this->fileSource); + } + + public function unlink($path) { + $this->setup(); + $path = substr($path, strlen('oc://')); + return self::$rootView->unlink($path); + } + + public function dir_opendir($path, $options) { + $this->setup(); + $path = substr($path, strlen('oc://')); + $this->path = $path; + $this->dirSource = self::$rootView->opendir($path); + if (is_resource($this->dirSource)) { + $this->meta = stream_get_meta_data($this->dirSource); + } + return is_resource($this->dirSource); + } + + public function dir_readdir() { + return readdir($this->dirSource); + } + + public function dir_closedir() { + closedir($this->dirSource); + } + + public function dir_rewinddir() { + rewinddir($this->dirSource); + } +} diff --git a/lib/private/files/stream/quota.php b/lib/private/files/stream/quota.php new file mode 100644 index 00000000000..60e60da8e67 --- /dev/null +++ b/lib/private/files/stream/quota.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Stream; + +/** + * stream wrapper limits the amount of data that can be written to a stream + * + * usage: void \OC\Files\Stream\Quota::register($id, $stream, $limit) + * or: resource \OC\Files\Stream\Quota::wrap($stream, $limit) + */ +class Quota { + private static $streams = array(); + + /** + * @var resource $source + */ + private $source; + + /** + * @var int $limit + */ + private $limit; + + /** + * @param string $id + * @param resource $stream + * @param int $limit + */ + public static function register($id, $stream, $limit) { + self::$streams[$id] = array($stream, $limit); + } + + /** + * remove all registered streams + */ + public static function clear() { + self::$streams = array(); + } + + /** + * @param resource $stream + * @param int $limit + * @return resource + */ + static public function wrap($stream, $limit) { + $id = uniqid(); + self::register($id, $stream, $limit); + $meta = stream_get_meta_data($stream); + return fopen('quota://' . $id, $meta['mode']); + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $id = substr($path, strlen('quota://')); + if (isset(self::$streams[$id])) { + list($this->source, $this->limit) = self::$streams[$id]; + return true; + } else { + return false; + } + } + + public function stream_seek($offset, $whence = SEEK_SET) { + if ($whence === SEEK_END){ + // go to the end to find out last position's offset + $oldOffset = $this->stream_tell(); + if (fseek($this->source, 0, $whence) !== 0){ + return false; + } + $whence = SEEK_SET; + $offset = $this->stream_tell() + $offset; + $this->limit += $oldOffset - $offset; + } + else if ($whence === SEEK_SET) { + $this->limit += $this->stream_tell() - $offset; + } else { + $this->limit -= $offset; + } + // this wrapper needs to return "true" for success. + // the fseek call itself returns 0 on succeess + return !fseek($this->source, $offset, $whence); + } + + public function stream_tell() { + return ftell($this->source); + } + + public function stream_read($count) { + $this->limit -= $count; + return fread($this->source, $count); + } + + public function stream_write($data) { + $size = strlen($data); + if ($size > $this->limit) { + $data = substr($data, 0, $this->limit); + $size = $this->limit; + } + $this->limit -= $size; + return fwrite($this->source, $data); + } + + public function stream_set_option($option, $arg1, $arg2) { + switch ($option) { + case STREAM_OPTION_BLOCKING: + stream_set_blocking($this->source, $arg1); + break; + case STREAM_OPTION_READ_TIMEOUT: + stream_set_timeout($this->source, $arg1, $arg2); + break; + case STREAM_OPTION_WRITE_BUFFER: + stream_set_write_buffer($this->source, $arg1, $arg2); + } + } + + public function stream_stat() { + return fstat($this->source); + } + + public function stream_lock($mode) { + flock($this->source, $mode); + } + + public function stream_flush() { + return fflush($this->source); + } + + public function stream_eof() { + return feof($this->source); + } + + public function stream_close() { + fclose($this->source); + } +} diff --git a/lib/private/files/stream/staticstream.php b/lib/private/files/stream/staticstream.php new file mode 100644 index 00000000000..45b1a7a81f8 --- /dev/null +++ b/lib/private/files/stream/staticstream.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Stream; + +class StaticStream { + const MODE_FILE = 0100000; + + public $context; + protected static $data = array(); + + protected $path = ''; + protected $pointer = 0; + protected $writable = false; + + public function stream_close() { + } + + public function stream_eof() { + return $this->pointer >= strlen(self::$data[$this->path]); + } + + public function stream_flush() { + } + + public static function clear() { + self::$data = array(); + } + + public function stream_open($path, $mode, $options, &$opened_path) { + switch ($mode[0]) { + case 'r': + if (!isset(self::$data[$path])) return false; + $this->path = $path; + $this->writable = isset($mode[1]) && $mode[1] == '+'; + break; + case 'w': + self::$data[$path] = ''; + $this->path = $path; + $this->writable = true; + break; + case 'a': + if (!isset(self::$data[$path])) self::$data[$path] = ''; + $this->path = $path; + $this->writable = true; + $this->pointer = strlen(self::$data[$path]); + break; + case 'x': + if (isset(self::$data[$path])) return false; + $this->path = $path; + $this->writable = true; + break; + case 'c': + if (!isset(self::$data[$path])) self::$data[$path] = ''; + $this->path = $path; + $this->writable = true; + break; + default: + return false; + } + $opened_path = $this->path; + return true; + } + + public function stream_read($count) { + $bytes = min(strlen(self::$data[$this->path]) - $this->pointer, $count); + $data = substr(self::$data[$this->path], $this->pointer, $bytes); + $this->pointer += $bytes; + return $data; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + $len = strlen(self::$data[$this->path]); + switch ($whence) { + case SEEK_SET: + if ($offset <= $len) { + $this->pointer = $offset; + return true; + } + break; + case SEEK_CUR: + if ($this->pointer + $offset <= $len) { + $this->pointer += $offset; + return true; + } + break; + case SEEK_END: + if ($len + $offset <= $len) { + $this->pointer = $len + $offset; + return true; + } + break; + } + return false; + } + + public function stream_stat() { + return $this->url_stat($this->path); + } + + public function stream_tell() { + return $this->pointer; + } + + public function stream_write($data) { + if (!$this->writable) return 0; + $size = strlen($data); + if ($this->stream_eof()) { + self::$data[$this->path] .= $data; + } else { + self::$data[$this->path] = substr_replace( + self::$data[$this->path], + $data, + $this->pointer + ); + } + $this->pointer += $size; + return $size; + } + + public function unlink($path) { + if (isset(self::$data[$path])) { + unset(self::$data[$path]); + } + return true; + } + + public function url_stat($path) { + if (isset(self::$data[$path])) { + $size = strlen(self::$data[$path]); + $time = time(); + $data = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => self::MODE_FILE | 0777, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => '', + 'size' => $size, + 'atime' => $time, + 'mtime' => $time, + 'ctime' => $time, + 'blksize' => -1, + 'blocks' => -1, + ); + return array_values($data) + $data; + } + return false; + } +} diff --git a/lib/private/files/type/detection.php b/lib/private/files/type/detection.php new file mode 100644 index 00000000000..d7cc9ebbf4e --- /dev/null +++ b/lib/private/files/type/detection.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Type; + +/** + * Class Detection + * + * Mimetype detection + * + * @package OC\Files\Type + */ +class Detection { + protected $mimetypes = array(); + + /** + * add an extension -> mimetype mapping + * + * @param string $extension + * @param string $mimetype + */ + public function registerType($extension, $mimetype) { + $this->mimetypes[$extension] = $mimetype; + } + + /** + * add an array of extension -> mimetype mappings + * + * @param array $types + */ + public function registerTypeArray($types) { + $this->mimetypes = array_merge($this->mimetypes, $types); + } + + /** + * detect mimetype only based on filename, content of file is not used + * + * @param string $path + * @return string + */ + public function detectPath($path) { + if (strpos($path, '.')) { + //try to guess the type by the file extension + $extension = strtolower(strrchr(basename($path), ".")); + $extension = substr($extension, 1); //remove leading . + return (isset($this->mimetypes[$extension])) ? $this->mimetypes[$extension] : 'application/octet-stream'; + } else { + return 'application/octet-stream'; + } + } + + /** + * detect mimetype based on both filename and content + * + * @param string $path + * @return string + */ + public function detect($path) { + if (@is_dir($path)) { + // directories are easy + return "httpd/unix-directory"; + } + + $mimeType = $this->detectPath($path); + + if ($mimeType === 'application/octet-stream' and function_exists('finfo_open') + and function_exists('finfo_file') and $finfo = finfo_open(FILEINFO_MIME) + ) { + $info = @strtolower(finfo_file($finfo, $path)); + if ($info) { + $mimeType = substr($info, 0, strpos($info, ';')); + return empty($mimeType) ? 'application/octet-stream' : $mimeType; + } + finfo_close($finfo); + } + $isWrapped = (strpos($path, '://') !== false) and (substr($path, 0, 7) === 'file://'); + if (!$isWrapped and $mimeType === 'application/octet-stream' && function_exists("mime_content_type")) { + // use mime magic extension if available + $mimeType = mime_content_type($path); + } + if (!$isWrapped and $mimeType === 'application/octet-stream' && \OC_Helper::canExecute("file")) { + // it looks like we have a 'file' command, + // lets see if it does have mime support + $path = escapeshellarg($path); + $fp = popen("file -b --mime-type $path 2>/dev/null", "r"); + $reply = fgets($fp); + pclose($fp); + + //trim the newline + $mimeType = trim($reply); + + if (empty($mimeType)) { + $mimeType = 'application/octet-stream'; + } + + } + return $mimeType; + } + + /** + * detect mimetype based on the content of a string + * + * @param string $data + * @return string + */ + public function detectString($data) { + if (function_exists('finfo_open') and function_exists('finfo_file')) { + $finfo = finfo_open(FILEINFO_MIME); + return finfo_buffer($finfo, $data); + } else { + $tmpFile = \OC_Helper::tmpFile(); + $fh = fopen($tmpFile, 'wb'); + fwrite($fh, $data, 8024); + fclose($fh); + $mime = $this->detect($tmpFile); + unset($tmpFile); + return $mime; + } + } +} diff --git a/lib/private/files/type/templatemanager.php b/lib/private/files/type/templatemanager.php new file mode 100644 index 00000000000..cd1536d2732 --- /dev/null +++ b/lib/private/files/type/templatemanager.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Type; + +class TemplateManager { + protected $templates = array(); + + public function registerTemplate($mimetype, $path) { + $this->templates[$mimetype] = $path; + } + + /** + * get the path of the template for a mimetype + * + * @param string $mimetype + * @return string | null + */ + public function getTemplatePath($mimetype) { + if (isset($this->templates[$mimetype])) { + return $this->templates[$mimetype]; + } else { + return null; + } + } + + /** + * get the template content for a mimetype + * + * @param string $mimetype + * @return string + */ + public function getTemplate($mimetype) { + $path = $this->getTemplatePath($mimetype); + if ($path) { + return file_get_contents($path); + } else { + return ''; + } + } +} diff --git a/lib/private/files/utils/scanner.php b/lib/private/files/utils/scanner.php new file mode 100644 index 00000000000..2cad7dd77bd --- /dev/null +++ b/lib/private/files/utils/scanner.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Utils; + +use OC\Files\Filesystem; +use OC\Hooks\PublicEmitter; + +/** + * Class Scanner + * + * Hooks available in scope \OC\Utils\Scanner + * - scanFile(string $absolutePath) + * - scanFolder(string $absolutePath) + * + * @package OC\Files\Utils + */ +class Scanner extends PublicEmitter { + /** + * @var string $user + */ + private $user; + + /** + * @param string $user + */ + public function __construct($user) { + $this->user = $user; + } + + /** + * get all storages for $dir + * + * @param string $dir + * @return \OC\Files\Mount\Mount[] + */ + protected function getMounts($dir) { + //TODO: move to the node based fileapi once that's done + \OC_Util::tearDownFS(); + \OC_Util::setupFS($this->user); + $absolutePath = Filesystem::getView()->getAbsolutePath($dir); + + $mountManager = Filesystem::getMountManager(); + $mounts = $mountManager->findIn($absolutePath); + $mounts[] = $mountManager->find($absolutePath); + $mounts = array_reverse($mounts); //start with the mount of $dir + + return $mounts; + } + + /** + * attach listeners to the scanner + * + * @param \OC\Files\Mount\Mount $mount + */ + protected function attachListener($mount) { + $scanner = $mount->getStorage()->getScanner(); + $emitter = $this; + $scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function ($path) use ($mount, $emitter) { + $emitter->emit('\OC\Files\Utils\Scanner', 'scanFile', array($mount->getMountPoint() . $path)); + }); + $scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function ($path) use ($mount, $emitter) { + $emitter->emit('\OC\Files\Utils\Scanner', 'scanFolder', array($mount->getMountPoint() . $path)); + }); + } + + public function backgroundScan($dir) { + $mounts = $this->getMounts($dir); + foreach ($mounts as $mount) { + if (is_null($mount->getStorage())) { + continue; + } + $scanner = $mount->getStorage()->getScanner(); + $this->attachListener($mount); + $scanner->backgroundScan(); + } + } + + public function scan($dir) { + $mounts = $this->getMounts($dir); + foreach ($mounts as $mount) { + if (is_null($mount->getStorage())) { + continue; + } + $scanner = $mount->getStorage()->getScanner(); + $this->attachListener($mount); + $scanner->scan('', \OC\Files\Cache\Scanner::SCAN_RECURSIVE, \OC\Files\Cache\Scanner::REUSE_ETAG); + } + } +} + diff --git a/lib/private/files/view.php b/lib/private/files/view.php new file mode 100644 index 00000000000..ac45a881331 --- /dev/null +++ b/lib/private/files/view.php @@ -0,0 +1,1091 @@ +<?php +/** + * Copyright (c) 2012 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +/** + * Class to provide access to ownCloud filesystem via a "view", and methods for + * working with files within that view (e.g. read, write, delete, etc.). Each + * view is restricted to a set of directories via a virtual root. The default view + * uses the currently logged in user's data directory as root (parts of + * OC_Filesystem are merely a wrapper for OC_FilesystemView). + * + * Apps that need to access files outside of the user data folders (to modify files + * belonging to a user other than the one currently logged in, for example) should + * use this class directly rather than using OC_Filesystem, or making use of PHP's + * built-in file manipulation functions. This will ensure all hooks and proxies + * are triggered correctly. + * + * Filesystem functions are not called directly; they are passed to the correct + * \OC\Files\Storage\Storage object + */ + +namespace OC\Files; + +class View { + private $fakeRoot = ''; + private $internal_path_cache = array(); + private $storage_cache = array(); + + public function __construct($root = '') { + $this->fakeRoot = $root; + } + + public function getAbsolutePath($path = '/') { + if (!$path) { + $path = '/'; + } + if ($path[0] !== '/') { + $path = '/' . $path; + } + return $this->fakeRoot . $path; + } + + /** + * change the root to a fake root + * + * @param string $fakeRoot + * @return bool + */ + public function chroot($fakeRoot) { + if (!$fakeRoot == '') { + if ($fakeRoot[0] !== '/') { + $fakeRoot = '/' . $fakeRoot; + } + } + $this->fakeRoot = $fakeRoot; + } + + /** + * get the fake root + * + * @return string + */ + public function getRoot() { + return $this->fakeRoot; + } + + /** + * get path relative to the root of the view + * + * @param string $path + * @return string + */ + public function getRelativePath($path) { + if ($this->fakeRoot == '') { + return $path; + } + if (strpos($path, $this->fakeRoot) !== 0) { + return null; + } else { + $path = substr($path, strlen($this->fakeRoot)); + if (strlen($path) === 0) { + return '/'; + } else { + return $path; + } + } + } + + /** + * get the mountpoint of the storage object for a path + * ( note: because a storage is not always mounted inside the fakeroot, the + * returned mountpoint is relative to the absolute root of the filesystem + * and doesn't take the chroot into account ) + * + * @param string $path + * @return string + */ + public function getMountPoint($path) { + return Filesystem::getMountPoint($this->getAbsolutePath($path)); + } + + /** + * resolve a path to a storage and internal path + * + * @param string $path + * @return array consisting of the storage and the internal path + */ + public function resolvePath($path) { + $a = $this->getAbsolutePath($path); + $p = Filesystem::normalizePath($a); + return Filesystem::resolvePath($p); + } + + /** + * return the path to a local version of the file + * we need this because we can't know if a file is stored local or not from + * outside the filestorage and for some purposes a local file is needed + * + * @param string $path + * @return string + */ + public function getLocalFile($path) { + $parent = substr($path, 0, strrpos($path, '/')); + $path = $this->getAbsolutePath($path); + list($storage, $internalPath) = Filesystem::resolvePath($path); + if (Filesystem::isValidPath($parent) and $storage) { + return $storage->getLocalFile($internalPath); + } else { + return null; + } + } + + /** + * @param string $path + * @return string + */ + public function getLocalFolder($path) { + $parent = substr($path, 0, strrpos($path, '/')); + $path = $this->getAbsolutePath($path); + list($storage, $internalPath) = Filesystem::resolvePath($path); + if (Filesystem::isValidPath($parent) and $storage) { + return $storage->getLocalFolder($internalPath); + } else { + return null; + } + } + + /** + * the following functions operate with arguments and return values identical + * to those of their PHP built-in equivalents. Mostly they are merely wrappers + * for \OC\Files\Storage\Storage via basicOperation(). + */ + public function mkdir($path) { + return $this->basicOperation('mkdir', $path, array('create', 'write')); + } + + public function rmdir($path) { + if ($this->is_dir($path)) { + return $this->basicOperation('rmdir', $path, array('delete')); + } else { + return false; + } + } + + public function opendir($path) { + return $this->basicOperation('opendir', $path, array('read')); + } + + public function readdir($handle) { + $fsLocal = new Storage\Local(array('datadir' => '/')); + return $fsLocal->readdir($handle); + } + + public function is_dir($path) { + if ($path == '/') { + return true; + } + return $this->basicOperation('is_dir', $path); + } + + public function is_file($path) { + if ($path == '/') { + return false; + } + return $this->basicOperation('is_file', $path); + } + + public function stat($path) { + return $this->basicOperation('stat', $path); + } + + public function filetype($path) { + return $this->basicOperation('filetype', $path); + } + + public function filesize($path) { + return $this->basicOperation('filesize', $path); + } + + public function readfile($path) { + @ob_end_clean(); + $handle = $this->fopen($path, 'rb'); + if ($handle) { + $chunkSize = 8192; // 8 kB chunks + while (!feof($handle)) { + echo fread($handle, $chunkSize); + flush(); + } + $size = $this->filesize($path); + return $size; + } + return false; + } + + public function isCreatable($path) { + return $this->basicOperation('isCreatable', $path); + } + + public function isReadable($path) { + return $this->basicOperation('isReadable', $path); + } + + public function isUpdatable($path) { + return $this->basicOperation('isUpdatable', $path); + } + + public function isDeletable($path) { + return $this->basicOperation('isDeletable', $path); + } + + public function isSharable($path) { + return $this->basicOperation('isSharable', $path); + } + + public function file_exists($path) { + if ($path == '/') { + return true; + } + return $this->basicOperation('file_exists', $path); + } + + public function filemtime($path) { + return $this->basicOperation('filemtime', $path); + } + + public function touch($path, $mtime = null) { + if (!is_null($mtime) and !is_numeric($mtime)) { + $mtime = strtotime($mtime); + } + + $hooks = array('touch'); + + if (!$this->file_exists($path)) { + $hooks[] = 'create'; + $hooks[] = 'write'; + } + $result = $this->basicOperation('touch', $path, $hooks, $mtime); + if (!$result) { //if native touch fails, we emulate it by changing the mtime in the cache + $this->putFileInfo($path, array('mtime' => $mtime)); + } + return true; + } + + public function file_get_contents($path) { + return $this->basicOperation('file_get_contents', $path, array('read')); + } + + public function file_put_contents($path, $data) { + if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier + $absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path)); + if (\OC_FileProxy::runPreProxies('file_put_contents', $absolutePath, $data) + and Filesystem::isValidPath($path) + and !Filesystem::isFileBlacklisted($path) + ) { + $path = $this->getRelativePath($absolutePath); + $exists = $this->file_exists($path); + $run = true; + if ($this->shouldEmitHooks($path)) { + if (!$exists) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_create, + array( + Filesystem::signal_param_path => $this->getHookPath($path), + Filesystem::signal_param_run => &$run + ) + ); + } + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_write, + array( + Filesystem::signal_param_path => $this->getHookPath($path), + Filesystem::signal_param_run => &$run + ) + ); + } + if (!$run) { + return false; + } + $target = $this->fopen($path, 'w'); + if ($target) { + list ($count, $result) = \OC_Helper::streamCopy($data, $target); + fclose($target); + fclose($data); + if ($this->shouldEmitHooks($path) && $result !== false) { + if (!$exists) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_create, + array(Filesystem::signal_param_path => $this->getHookPath($path)) + ); + } + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_write, + array(Filesystem::signal_param_path => $this->getHookPath($path)) + ); + } + \OC_FileProxy::runPostProxies('file_put_contents', $absolutePath, $count); + return $result; + } else { + return false; + } + } else { + return false; + } + } else { + $hooks = ($this->file_exists($path)) ? array('write') : array('create', 'write'); + return $this->basicOperation('file_put_contents', $path, $hooks, $data); + } + } + + public function unlink($path) { + return $this->basicOperation('unlink', $path, array('delete')); + } + + public function deleteAll($directory, $empty = false) { + return $this->rmdir($directory); + } + + public function rename($path1, $path2) { + $postFix1 = (substr($path1, -1, 1) === '/') ? '/' : ''; + $postFix2 = (substr($path2, -1, 1) === '/') ? '/' : ''; + $absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1)); + $absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2)); + if ( + \OC_FileProxy::runPreProxies('rename', $absolutePath1, $absolutePath2) + and Filesystem::isValidPath($path2) + and Filesystem::isValidPath($path1) + and !Filesystem::isFileBlacklisted($path2) + ) { + $path1 = $this->getRelativePath($absolutePath1); + $path2 = $this->getRelativePath($absolutePath2); + + if ($path1 == null or $path2 == null) { + return false; + } + $run = true; + if ($this->shouldEmitHooks() && (Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2))) { + // if it was a rename from a part file to a regular file it was a write and not a rename operation + \OC_Hook::emit( + Filesystem::CLASSNAME, Filesystem::signal_write, + array( + Filesystem::signal_param_path => $this->getHookPath($path2), + Filesystem::signal_param_run => &$run + ) + ); + } elseif ($this->shouldEmitHooks()) { + \OC_Hook::emit( + Filesystem::CLASSNAME, Filesystem::signal_rename, + array( + Filesystem::signal_param_oldpath => $this->getHookPath($path1), + Filesystem::signal_param_newpath => $this->getHookPath($path2), + Filesystem::signal_param_run => &$run + ) + ); + } + if ($run) { + $mp1 = $this->getMountPoint($path1 . $postFix1); + $mp2 = $this->getMountPoint($path2 . $postFix2); + if ($mp1 == $mp2) { + list($storage, $internalPath1) = Filesystem::resolvePath($absolutePath1 . $postFix1); + list(, $internalPath2) = Filesystem::resolvePath($absolutePath2 . $postFix2); + if ($storage) { + $result = $storage->rename($internalPath1, $internalPath2); + \OC_FileProxy::runPostProxies('rename', $absolutePath1, $absolutePath2); + } else { + $result = false; + } + } else { + if ($this->is_dir($path1)) { + $result = $this->copy($path1, $path2); + if ($result === true) { + list($storage1, $internalPath1) = Filesystem::resolvePath($absolutePath1 . $postFix1); + $result = $storage1->deleteAll($internalPath1); + } + } else { + $source = $this->fopen($path1 . $postFix1, 'r'); + $target = $this->fopen($path2 . $postFix2, 'w'); + list($count, $result) = \OC_Helper::streamCopy($source, $target); + + // close open handle - especially $source is necessary because unlink below will + // throw an exception on windows because the file is locked + fclose($source); + fclose($target); + + if ($result !== false) { + list($storage1, $internalPath1) = Filesystem::resolvePath($absolutePath1 . $postFix1); + $storage1->unlink($internalPath1); + } + } + } + if ($this->shouldEmitHooks() && (Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) { + // if it was a rename from a part file to a regular file it was a write and not a rename operation + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_write, + array( + Filesystem::signal_param_path => $this->getHookPath($path2), + ) + ); + } elseif ($this->shouldEmitHooks() && $result !== false) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_rename, + array( + Filesystem::signal_param_oldpath => $this->getHookPath($path1), + Filesystem::signal_param_newpath => $this->getHookPath($path2) + ) + ); + } + return $result; + } else { + return false; + } + } else { + return false; + } + } + + public function copy($path1, $path2) { + $postFix1 = (substr($path1, -1, 1) === '/') ? '/' : ''; + $postFix2 = (substr($path2, -1, 1) === '/') ? '/' : ''; + $absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($path1)); + $absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($path2)); + if ( + \OC_FileProxy::runPreProxies('copy', $absolutePath1, $absolutePath2) + and Filesystem::isValidPath($path2) + and Filesystem::isValidPath($path1) + and !Filesystem::isFileBlacklisted($path2) + ) { + $path1 = $this->getRelativePath($absolutePath1); + $path2 = $this->getRelativePath($absolutePath2); + + if ($path1 == null or $path2 == null) { + return false; + } + $run = true; + $exists = $this->file_exists($path2); + if ($this->shouldEmitHooks()) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_copy, + array( + Filesystem::signal_param_oldpath => $this->getHookPath($path1), + Filesystem::signal_param_newpath => $this->getHookPath($path2), + Filesystem::signal_param_run => &$run + ) + ); + if ($run and !$exists) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_create, + array( + Filesystem::signal_param_path => $this->getHookPath($path2), + Filesystem::signal_param_run => &$run + ) + ); + } + if ($run) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_write, + array( + Filesystem::signal_param_path => $this->getHookPath($path2), + Filesystem::signal_param_run => &$run + ) + ); + } + } + if ($run) { + $mp1 = $this->getMountPoint($path1 . $postFix1); + $mp2 = $this->getMountPoint($path2 . $postFix2); + if ($mp1 == $mp2) { + list($storage, $internalPath1) = Filesystem::resolvePath($absolutePath1 . $postFix1); + list(, $internalPath2) = Filesystem::resolvePath($absolutePath2 . $postFix2); + if ($storage) { + $result = $storage->copy($internalPath1, $internalPath2); + } else { + $result = false; + } + } else { + if ($this->is_dir($path1) && ($dh = $this->opendir($path1))) { + $result = $this->mkdir($path2); + if (is_resource($dh)) { + while (($file = readdir($dh)) !== false) { + if (!Filesystem::isIgnoredDir($file)) { + $result = $this->copy($path1 . '/' . $file, $path2 . '/' . $file); + } + } + } + } else { + $source = $this->fopen($path1 . $postFix1, 'r'); + $target = $this->fopen($path2 . $postFix2, 'w'); + list($count, $result) = \OC_Helper::streamCopy($source, $target); + } + } + if ($this->shouldEmitHooks() && $result !== false) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_copy, + array( + Filesystem::signal_param_oldpath => $this->getHookPath($path1), + Filesystem::signal_param_newpath => $this->getHookPath($path2) + ) + ); + if (!$exists) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_create, + array(Filesystem::signal_param_path => $this->getHookPath($path2)) + ); + } + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_post_write, + array(Filesystem::signal_param_path => $this->getHookPath($path2)) + ); + } + return $result; + } else { + return false; + } + } else { + return false; + } + } + + public function fopen($path, $mode) { + $hooks = array(); + switch ($mode) { + case 'r': + case 'rb': + $hooks[] = 'read'; + break; + case 'r+': + case 'rb+': + case 'w+': + case 'wb+': + case 'x+': + case 'xb+': + case 'a+': + case 'ab+': + $hooks[] = 'read'; + $hooks[] = 'write'; + break; + case 'w': + case 'wb': + case 'x': + case 'xb': + case 'a': + case 'ab': + $hooks[] = 'write'; + break; + default: + \OC_Log::write('core', 'invalid mode (' . $mode . ') for ' . $path, \OC_Log::ERROR); + } + + return $this->basicOperation('fopen', $path, $hooks, $mode); + } + + public function toTmpFile($path) { + if (Filesystem::isValidPath($path)) { + $source = $this->fopen($path, 'r'); + if ($source) { + $extension = pathinfo($path, PATHINFO_EXTENSION); + $tmpFile = \OC_Helper::tmpFile($extension); + file_put_contents($tmpFile, $source); + return $tmpFile; + } else { + return false; + } + } else { + return false; + } + } + + public function fromTmpFile($tmpFile, $path) { + if (Filesystem::isValidPath($path)) { + if (!$tmpFile) { + debug_print_backtrace(); + } + $source = fopen($tmpFile, 'r'); + if ($source) { + $this->file_put_contents($path, $source); + unlink($tmpFile); + return true; + } else { + return false; + } + } else { + return false; + } + } + + public function getMimeType($path) { + return $this->basicOperation('getMimeType', $path); + } + + public function hash($type, $path, $raw = false) { + $postFix = (substr($path, -1, 1) === '/') ? '/' : ''; + $absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path)); + if (\OC_FileProxy::runPreProxies('hash', $absolutePath) && Filesystem::isValidPath($path)) { + $path = $this->getRelativePath($absolutePath); + if ($path == null) { + return false; + } + if ($this->shouldEmitHooks($path)) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + Filesystem::signal_read, + array(Filesystem::signal_param_path => $this->getHookPath($path)) + ); + } + list($storage, $internalPath) = Filesystem::resolvePath($absolutePath . $postFix); + if ($storage) { + $result = $storage->hash($type, $internalPath, $raw); + $result = \OC_FileProxy::runPostProxies('hash', $absolutePath, $result); + return $result; + } + } + return null; + } + + public function free_space($path = '/') { + return $this->basicOperation('free_space', $path); + } + + /** + * @brief abstraction layer for basic filesystem functions: wrapper for \OC\Files\Storage\Storage + * @param string $operation + * @param string $path + * @param array $hooks (optional) + * @param mixed $extraParam (optional) + * @return mixed + * + * This method takes requests for basic filesystem functions (e.g. reading & writing + * files), processes hooks and proxies, sanitises paths, and finally passes them on to + * \OC\Files\Storage\Storage for delegation to a storage backend for execution + */ + private function basicOperation($operation, $path, $hooks = array(), $extraParam = null) { + $postFix = (substr($path, -1, 1) === '/') ? '/' : ''; + $absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path)); + if (\OC_FileProxy::runPreProxies($operation, $absolutePath, $extraParam) + and Filesystem::isValidPath($path) + and !Filesystem::isFileBlacklisted($path) + ) { + $path = $this->getRelativePath($absolutePath); + if ($path == null) { + return false; + } + + $run = $this->runHooks($hooks, $path); + list($storage, $internalPath) = Filesystem::resolvePath($absolutePath . $postFix); + if ($run and $storage) { + if (!is_null($extraParam)) { + $result = $storage->$operation($internalPath, $extraParam); + } else { + $result = $storage->$operation($internalPath); + } + $result = \OC_FileProxy::runPostProxies($operation, $this->getAbsolutePath($path), $result); + if ($this->shouldEmitHooks($path) && $result !== false) { + if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open + $this->runHooks($hooks, $path, true); + } + } + return $result; + } + } + return null; + } + + /** + * get the path relative to the default root for hook usage + * + * @param string $path + * @return string + */ + private function getHookPath($path) { + if (!Filesystem::getView()) { + return $path; + } + return Filesystem::getView()->getRelativePath($this->getAbsolutePath($path)); + } + + private function shouldEmitHooks($path = '') { + if ($path && Cache\Scanner::isPartialFile($path)) { + return false; + } + if (!Filesystem::$loaded) { + return false; + } + $defaultRoot = Filesystem::getRoot(); + if ($this->fakeRoot === $defaultRoot) { + return true; + } + return (strlen($this->fakeRoot) > strlen($defaultRoot)) && (substr($this->fakeRoot, 0, strlen($defaultRoot) + 1) === $defaultRoot . '/'); + } + + private function runHooks($hooks, $path, $post = false) { + $path = $this->getHookPath($path); + $prefix = ($post) ? 'post_' : ''; + $run = true; + if ($this->shouldEmitHooks($path)) { + foreach ($hooks as $hook) { + if ($hook != 'read') { + \OC_Hook::emit( + Filesystem::CLASSNAME, + $prefix . $hook, + array( + Filesystem::signal_param_run => &$run, + Filesystem::signal_param_path => $path + ) + ); + } elseif (!$post) { + \OC_Hook::emit( + Filesystem::CLASSNAME, + $prefix . $hook, + array( + Filesystem::signal_param_path => $path + ) + ); + } + } + } + return $run; + } + + /** + * check if a file or folder has been updated since $time + * + * @param string $path + * @param int $time + * @return bool + */ + public function hasUpdated($path, $time) { + return $this->basicOperation('hasUpdated', $path, array(), $time); + } + + /** + * get the filesystem info + * + * @param string $path + * @param boolean $includeMountPoints whether to add mountpoint sizes, + * defaults to true + * @return array + * + * returns an associative array with the following keys: + * - size + * - mtime + * - mimetype + * - encrypted + * - versioned + */ + public function getFileInfo($path, $includeMountPoints = true) { + $data = array(); + if (!Filesystem::isValidPath($path)) { + return $data; + } + $path = Filesystem::normalizePath($this->fakeRoot . '/' . $path); + /** + * @var \OC\Files\Storage\Storage $storage + * @var string $internalPath + */ + list($storage, $internalPath) = Filesystem::resolvePath($path); + if ($storage) { + $cache = $storage->getCache($internalPath); + $permissionsCache = $storage->getPermissionsCache($internalPath); + $user = \OC_User::getUser(); + + if (!$cache->inCache($internalPath)) { + $scanner = $storage->getScanner($internalPath); + $scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW); + } else { + $watcher = $storage->getWatcher($internalPath); + $watcher->checkUpdate($internalPath); + } + + $data = $cache->get($internalPath); + + if ($data and $data['fileid']) { + if ($includeMountPoints and $data['mimetype'] === 'httpd/unix-directory') { + //add the sizes of other mountpoints to the folder + $mountPoints = Filesystem::getMountPoints($path); + foreach ($mountPoints as $mountPoint) { + $subStorage = Filesystem::getStorage($mountPoint); + if ($subStorage) { + $subCache = $subStorage->getCache(''); + $rootEntry = $subCache->get(''); + $data['size'] += isset($rootEntry['size']) ? $rootEntry['size'] : 0; + } + } + } + + $permissions = $permissionsCache->get($data['fileid'], $user); + if ($permissions === -1) { + $permissions = $storage->getPermissions($internalPath); + $permissionsCache->set($data['fileid'], $user, $permissions); + } + $data['permissions'] = $permissions; + } + } + + $data = \OC_FileProxy::runPostProxies('getFileInfo', $path, $data); + + return $data; + } + + /** + * get the content of a directory + * + * @param string $directory path under datadirectory + * @param string $mimetype_filter limit returned content to this mimetype or mimepart + * @return array + */ + public function getDirectoryContent($directory, $mimetype_filter = '') { + $result = array(); + if (!Filesystem::isValidPath($directory)) { + return $result; + } + $path = Filesystem::normalizePath($this->fakeRoot . '/' . $directory); + /** + * @var \OC\Files\Storage\Storage $storage + * @var string $internalPath + */ + list($storage, $internalPath) = Filesystem::resolvePath($path); + if ($storage) { + $cache = $storage->getCache($internalPath); + $permissionsCache = $storage->getPermissionsCache($internalPath); + $user = \OC_User::getUser(); + + if ($cache->getStatus($internalPath) < Cache\Cache::COMPLETE) { + $scanner = $storage->getScanner($internalPath); + $scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW); + } else { + $watcher = $storage->getWatcher($internalPath); + $watcher->checkUpdate($internalPath); + } + + $files = $cache->getFolderContents($internalPath); //TODO: mimetype_filter + $permissions = $permissionsCache->getDirectoryPermissions($cache->getId($internalPath), $user); + + $ids = array(); + foreach ($files as $i => $file) { + $files[$i]['type'] = $file['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file'; + $ids[] = $file['fileid']; + + if (!isset($permissions[$file['fileid']])) { + $permissions[$file['fileid']] = $storage->getPermissions($file['path']); + $permissionsCache->set($file['fileid'], $user, $permissions[$file['fileid']]); + } + $files[$i]['permissions'] = $permissions[$file['fileid']]; + } + + //add a folder for any mountpoint in this directory and add the sizes of other mountpoints to the folders + $mountPoints = Filesystem::getMountPoints($path); + $dirLength = strlen($path); + foreach ($mountPoints as $mountPoint) { + $subStorage = Filesystem::getStorage($mountPoint); + if ($subStorage) { + $subCache = $subStorage->getCache(''); + + if ($subCache->getStatus('') === Cache\Cache::NOT_FOUND) { + $subScanner = $subStorage->getScanner(''); + $subScanner->scanFile(''); + } + + $rootEntry = $subCache->get(''); + if ($rootEntry) { + $relativePath = trim(substr($mountPoint, $dirLength), '/'); + if ($pos = strpos($relativePath, '/')) { + //mountpoint inside subfolder add size to the correct folder + $entryName = substr($relativePath, 0, $pos); + foreach ($files as &$entry) { + if ($entry['name'] === $entryName) { + $entry['size'] += $rootEntry['size']; + } + } + } else { //mountpoint in this folder, add an entry for it + $rootEntry['name'] = $relativePath; + $rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file'; + $subPermissionsCache = $subStorage->getPermissionsCache(''); + $permissions = $subPermissionsCache->get($rootEntry['fileid'], $user); + if ($permissions === -1) { + $permissions = $subStorage->getPermissions($rootEntry['path']); + $subPermissionsCache->set($rootEntry['fileid'], $user, $permissions); + } + // do not allow renaming/deleting the mount point + $rootEntry['permissions'] = $permissions & (\OCP\PERMISSION_ALL - (\OCP\PERMISSION_UPDATE | \OCP\PERMISSION_DELETE)); + + //remove any existing entry with the same name + foreach ($files as $i => $file) { + if ($file['name'] === $rootEntry['name']) { + unset($files[$i]); + break; + } + } + $files[] = $rootEntry; + } + } + } + } + + if ($mimetype_filter) { + foreach ($files as $file) { + if (strpos($mimetype_filter, '/')) { + if ($file['mimetype'] === $mimetype_filter) { + $result[] = $file; + } + } else { + if ($file['mimepart'] === $mimetype_filter) { + $result[] = $file; + } + } + } + } else { + $result = $files; + } + } + return $result; + } + + /** + * change file metadata + * + * @param string $path + * @param array $data + * @return int + * + * returns the fileid of the updated file + */ + public function putFileInfo($path, $data) { + $path = Filesystem::normalizePath($this->fakeRoot . '/' . $path); + /** + * @var \OC\Files\Storage\Storage $storage + * @var string $internalPath + */ + list($storage, $internalPath) = Filesystem::resolvePath($path); + if ($storage) { + $cache = $storage->getCache($path); + + if (!$cache->inCache($internalPath)) { + $scanner = $storage->getScanner($internalPath); + $scanner->scan($internalPath, Cache\Scanner::SCAN_SHALLOW); + } + + return $cache->put($internalPath, $data); + } else { + return -1; + } + } + + /** + * search for files with the name matching $query + * + * @param string $query + * @return array + */ + public function search($query) { + return $this->searchCommon('%' . $query . '%', 'search'); + } + + /** + * search for files by mimetype + * + * @param string $mimetype + * @return array + */ + public function searchByMime($mimetype) { + return $this->searchCommon($mimetype, 'searchByMime'); + } + + /** + * @param string $query + * @param string $method + * @return array + */ + private function searchCommon($query, $method) { + $files = array(); + $rootLength = strlen($this->fakeRoot); + + $mountPoint = Filesystem::getMountPoint($this->fakeRoot); + $storage = Filesystem::getStorage($mountPoint); + if ($storage) { + $cache = $storage->getCache(''); + + $results = $cache->$method($query); + foreach ($results as $result) { + if (substr($mountPoint . $result['path'], 0, $rootLength + 1) === $this->fakeRoot . '/') { + $result['path'] = substr($mountPoint . $result['path'], $rootLength); + $files[] = $result; + } + } + + $mountPoints = Filesystem::getMountPoints($this->fakeRoot); + foreach ($mountPoints as $mountPoint) { + $storage = Filesystem::getStorage($mountPoint); + if ($storage) { + $cache = $storage->getCache(''); + + $relativeMountPoint = substr($mountPoint, $rootLength); + $results = $cache->$method($query); + if ($results) { + foreach ($results as $result) { + $result['path'] = $relativeMountPoint . $result['path']; + $files[] = $result; + } + } + } + } + } + return $files; + } + + /** + * Get the owner for a file or folder + * + * @param string $path + * @return string + */ + public function getOwner($path) { + return $this->basicOperation('getOwner', $path); + } + + /** + * get the ETag for a file or folder + * + * @param string $path + * @return string + */ + public function getETag($path) { + /** + * @var Storage\Storage $storage + * @var string $internalPath + */ + list($storage, $internalPath) = $this->resolvePath($path); + if ($storage) { + return $storage->getETag($internalPath); + } else { + return null; + } + } + + /** + * Get the path of a file by id, relative to the view + * + * Note that the resulting path is not guarantied to be unique for the id, multiple paths can point to the same file + * + * @param int $id + * @return string + */ + public function getPath($id) { + list($storage, $internalPath) = Cache\Cache::getById($id); + $mounts = Filesystem::getMountByStorageId($storage); + foreach ($mounts as $mount) { + /** + * @var \OC\Files\Mount $mount + */ + $fullPath = $mount->getMountPoint() . $internalPath; + if (!is_null($path = $this->getRelativePath($fullPath))) { + return $path; + } + } + return null; + } +} |