Files
server/usr/share/psa-pear/pear/php/Horde/Vfs/Sql.php
2026-01-07 20:52:11 +01:00

842 lines
26 KiB
PHP

<?php
/**
* VFS implementation for Horde's database abstraction layer.
*
* Required values for $params:
* - db: A Horde_Db_Adapter object.
*
* Optional values:
* - table: (string) The name of the vfs table in 'database'. Defaults to
* 'horde_vfs'.
*
* The table structure for the VFS can be created with the horde-db-migrate
* script from the Horde_Db package.
*
* Copyright 2002-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file COPYING for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @author Chuck Hagenbuch <chuck@horde.org>
* @author Jan Schneider <jan@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package VFS
*/
class Horde_Vfs_Sql extends Horde_Vfs_Base
{
/* File value for vfs_type column. */
const FILE = 1;
/* Folder value for vfs_type column. */
const FOLDER = 2;
/**
* Handle for the current database connection.
*
* @var Horde_Db
*/
protected $_db = false;
/**
* List of permissions and if they can be changed in this VFS backend.
*
* @var array
*/
protected $_permissions = array();
/**
* List of features that the VFS driver supports.
*
* @var array
*/
protected $_features = array(
'readByteRange' => true,
);
/**
* Constructor.
*
* @param array $params A hash containing connection parameters.
*/
public function __construct($params = array())
{
$params = array_merge(array('table' => 'horde_vfs'), $params);
parent::__construct($params);
$this->_db = $this->_params['db'];
unset($this->_params['db']);
}
/**
* Retrieves the filesize from the VFS.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
*
* @return integer The file size.
* @throws Horde_Vfs_Exception
*/
public function size($path, $name)
{
$length_op = $this->_getFileSizeOp();
$path = $this->_convertPath($path);
list($op, $values) = $this->_nullString($path);
$sql = sprintf(
'SELECT %s(vfs_data) FROM %s WHERE vfs_path %s AND vfs_name = ?',
$length_op,
$this->_params['table'],
$op
);
$values[] = $name;
try {
$size = $this->_db->selectValue($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
if ($size === false) {
throw new Horde_Vfs_Exception(sprintf('Unable to check file size of "%s/%s".', $path, $name));
}
return $size;
}
/**
* Returns the size of a folder.
*
* @param string $path The path of the folder.
*
* @return integer The size of the folder in bytes.
* @throws Horde_Vfs_Exception
*/
public function getFolderSize($path = null)
{
try {
$where = null;
$params = array();
if (strlen($path)) {
$where = 'WHERE vfs_path = ? OR vfs_path LIKE ?';
$path = $this->_convertPath($path);
$params = array($path, $path . '/%');
}
$sql = sprintf('SELECT SUM(%s(vfs_data)) FROM %s %s',
$this->_getFileSizeOp(),
$this->_params['table'],
$where);
$size = $this->_db->selectValue($sql, $params);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
return (int)$size;
}
/**
* Retrieves a file from the VFS.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
*
* @return string The file data.
* @throws Horde_Vfs_Exception
*/
public function read($path, $name)
{
return $this->_readBlob($this->_params['table'], 'vfs_data', array(
'vfs_path' => $this->_convertPath($path),
'vfs_name' => $name
));
}
/**
* Retrieves a part of a file from the VFS. Particularly useful
* when reading large files which would exceed the PHP memory
* limits if they were stored in a string.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
* @param integer $offset The offset of the part. (The new offset will
* be stored in here).
* @param integer $length The length of the part. If the length = -1,
* the whole part after the offset is
* retrieved. If more bytes are given as exists
* after the given offset. Only the available
* bytes are read.
* @param integer $remaining The bytes that are left, after the part that
* is retrieved.
*
* @return string The file data.
* @throws Horde_Vfs_Exception
*/
public function readByteRange($path, $name, &$offset, $length, &$remaining)
{
$data = $this->_readBlob($this->_params['table'], 'vfs_data', array(
'vfs_path' => $this->_convertPath($path),
'vfs_name' => $name
));
// Calculate how many bytes MUST be read, so the remainging
// bytes and the new offset can be calculated correctly.
$size = strlen ($data);
if ($length == -1 || (($length + $offset) > $size)) {
$length = $size - $offset;
}
if ($remaining < 0) {
$remaining = 0;
}
$data = substr($data, $offset, $length);
$offset = $offset + $length;
$remaining = $size - $offset;
return $data;
}
/**
* Stores a file in the VFS.
*
* @param string $path The path to store the file in.
* @param string $name The filename to use.
* @param string $tmpFile The temporary file containing the data to
* be stored.
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function write($path, $name, $tmpFile, $autocreate = false)
{
/* Don't need to check quota here since it will be checked when
* writeData() is called. */
if (!$stream = @fopen($tmpFile, 'rb')) {
throw new Horde_Vfs_Exception('Unable to open ' . $tmpFile);
}
$result = $this->writeData(
$path, $name, $stream, $autocreate);
fclose($stream);
return $result;
}
/**
* Store a file in the VFS from raw data.
*
* @param string $path The path to store the file in.
* @param string $name The filename to use.
* @param string|resource $data The data as a string or stream resource.
* Resources allowed @since 2.4.0
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function writeData($path, $name, $data, $autocreate = false)
{
$data = $this->_ensureSeekable($data);
$this->_checkQuotaWrite('string', $data, $path, $name);
$path = $this->_convertPath($path);
list($op, $values) = $this->_nullString($path);
/* Check to see if the data already exists. */
try {
$sql = sprintf(
'SELECT vfs_id FROM %s WHERE vfs_path %s AND vfs_name = ?',
$this->_params['table'],
$op
);
$values[] = $name;
$id = $this->_db->selectValue($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
if ($id) {
$this->_updateBlob($this->_params['table'], 'vfs_data', $data,
array('vfs_id' => $id),
array('vfs_modified' => time()));
return;
}
/* Check to see if the folder already exists. */
$dirs = explode('/', $path);
$path_name = array_pop($dirs);
$parent = implode('/', $dirs);
if (!$this->isFolder($parent, $path_name)) {
if (!$autocreate) {
throw new Horde_Vfs_Exception(sprintf('Folder "%s" does not exist', $path));
}
$this->autocreatePath($path);
}
return $this->_insertBlob($this->_params['table'], 'vfs_data', $data, array(
'vfs_type' => self::FILE,
'vfs_path' => strlen($path) ? $path : null,
'vfs_name' => $name,
'vfs_modified' => time(),
'vfs_owner' => $this->_params['user']
));
}
/**
* Delete a file from the VFS.
*
* @param string $path The path to store the file in.
* @param string $name The filename to use.
*
* @throws Horde_Vfs_Exception
*/
public function deleteFile($path, $name)
{
$this->_checkQuotaDelete($path, $name);
$path = $this->_convertPath($path);
list($op, $values) = $this->_nullString($path);
try {
$sql = sprintf(
'DELETE FROM %s WHERE vfs_type = ? AND vfs_path %s AND vfs_name = ?',
$this->_params['table'],
$op
);
array_unshift($values, self::FILE);
array_push($values, $name);
$result = $this->_db->delete($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
if ($result == 0) {
throw new Horde_Vfs_Exception('Unable to delete VFS file.');
}
}
/**
* Rename a file or folder in the VFS.
*
* @param string $oldpath The old path to the file.
* @param string $oldname The old filename.
* @param string $newpath The new path of the file.
* @param string $newname The new filename.
*
* @throws Horde_Vfs_Exception
*/
public function rename($oldpath, $oldname, $newpath, $newname)
{
if (strpos($newpath, '/') === false) {
$parent = '';
$path = $newpath;
} else {
list($parent, $path) = explode('/', $newpath, 2);
}
if (!$this->isFolder($parent, $path)) {
$this->autocreatePath($newpath);
}
$oldpath = $this->_convertPath($oldpath);
$newpath = $this->_convertPath($newpath);
if (!strlen($newpath)) {
$newpath = null;
}
list($op, $values) = $this->_nullString($oldpath);
$sql = sprintf(
'UPDATE %s SET vfs_path = ?, vfs_name = ?, vfs_modified = ? WHERE vfs_path %s AND vfs_name = ?',
$this->_params['table'],
$op
);
$values = array_merge(
array($newpath, $newname, time()),
$values,
array($oldname)
);
try {
$result = $this->_db->update($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
if ($result == 0) {
throw new Horde_Vfs_Exception('Unable to rename VFS file.');
}
$this->_recursiveRename($oldpath, $oldname, $newpath, $newname);
}
/**
* Creates a folder on the VFS.
*
* @param string $path Holds the path of directory to create folder.
* @param string $name Holds the name of the new folder.
*
* @throws Horde_Vfs_Exception
*/
public function createFolder($path, $name)
{
$path = $this->_convertPath($path);
if (!strlen($path)) {
$path = null;
}
$sql = sprintf(
'INSERT INTO %s (vfs_type, vfs_path, vfs_name, vfs_modified, vfs_owner) VALUES (?, ?, ?, ?, ?)',
$this->_params['table']
);
$values = array(self::FOLDER, $path, $name, time(), $this->_params['user'] ?: null);
try {
$this->_db->insert($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
}
/**
* Horde_Vfs_Sql override of isFolder() to check for root folder.
*
* @param string $path Path to possible folder
* @param string $name Name of possible folder
*
* @return boolean True if $path/$name is a folder
*/
public function isFolder($path, $name)
{
$path = $this->_convertPath($path);
if ($path == '' && $name == '') {
// The root of VFS is always a folder.
return true;
}
$path = $this->_getNativePath($path, $name);
$name = basename($path);
$path = dirname($path);
if ($path == '.') {
$path = '';
}
list($op, $values) = $this->_nullString($path);
try {
return (bool)$this->_db->selectValue(
sprintf(
'SELECT 1 FROM %s WHERE vfs_type = ? AND vfs_path %s AND vfs_name = ?',
$this->_params['table'],
$op
),
array_merge(array(self::FOLDER), $values, array($name))
);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
}
/**
* Delete a folder from the VFS.
*
* @param string $path The path of the folder.
* @param string $name The folder name to use.
* @param boolean $recursive Force a recursive delete?
*
* @throws Horde_Vfs_Exception
*/
public function deleteFolder($path, $name, $recursive = false)
{
$path = $this->_convertPath($path);
$folderPath = $this->_getNativePath($path, $name);
/* Check if not recursive and fail if directory not empty */
if (!$recursive) {
$folderList = $this->listFolder($folderPath, null, true);
if (!empty($folderList)) {
throw new Horde_Vfs_Exception(sprintf('Unable to delete %s, the directory is not empty', $path . '/' . $name));
}
}
/* Remember the size of the folder. */
if (!is_null($this->_vfsSize)) {
$size = $this->getFolderSize($folderPath);
}
/* First delete everything below the folder, so if error we get no
* orphans. */
try {
$sql = sprintf('DELETE FROM %s WHERE vfs_path %s',
$this->_params['table'],
' LIKE ' . $this->_db->quote($this->_getNativePath($folderPath, '%')));
$this->_db->delete($sql);
} catch (Horde_Db_Exception $e) {
$this->_vfsSize = null;
throw new Horde_Vfs_Exception('Unable to delete VFS recursively: ' . $e->getMessage());
}
/* Now delete everything inside the folder. */
list ($op, $values) = $this->_nullString($folderPath);
try {
$sql = sprintf(
'DELETE FROM %s WHERE vfs_path %s',
$this->_params['table'],
$op
);
$this->_db->delete($sql, $values);
} catch (Horde_Db_Exception $e) {
$this->_vfsSize = null;
throw new Horde_Vfs_Exception('Unable to delete VFS directory: ' . $e->getMessage());
}
/* All ok now delete the actual folder */
list($op, $values) = $this->_nullString($path);
try {
$sql = sprintf(
'DELETE FROM %s WHERE vfs_path %s AND vfs_name = ?',
$this->_params['table'],
$op
);
$values[] = $name;
$this->_db->delete($sql, $values);
} catch (Horde_Db_Exception $e) {
$this->_vfsSize = null;
throw new Horde_Vfs_Exception('Unable to delete VFS directory: ' . $e->getMessage());
}
/* Update VFS size. */
if (!is_null($this->_vfsSize)) {
$this->_vfsSize -= $size;
}
}
/**
* Returns an an unsorted file list of the specified directory.
*
* @param string $path The path of the directory.
* @param string|array $filter Regular expression(s) to filter
* file/directory name on.
* @param boolean $dotfiles Show dotfiles?
* @param boolean $dironly Show only directories?
*
* @return array File list.
* @throws Horde_Vfs_Exception
*/
protected function _listFolder($path, $filter = null, $dotfiles = true,
$dironly = false)
{
if (!$this->isFolder(dirname($path), basename($path))) {
throw new Horde_Vfs_Exception(sprintf('"%s" is not a folder.', $path));
}
$path = $this->_convertPath($path);
list($op, $values) = $this->_nullString($path);
try {
$length_op = $this->_getFileSizeOp();
$sql = sprintf(
'SELECT vfs_name, vfs_type, %s(vfs_data) length, vfs_modified, vfs_owner FROM %s WHERE vfs_path %s',
$length_op,
$this->_params['table'],
$op
);
$fileList = $this->_db->select($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
$files = array();
foreach ($fileList as $line) {
// Filter out dotfiles if they aren't wanted.
if (!$dotfiles && substr($line['vfs_name'], 0, 1) == '.') {
continue;
}
$file['name'] = $line['vfs_name'];
if ($line['vfs_type'] == self::FILE) {
$name = explode('.', $line['vfs_name']);
if (count($name) == 1) {
$file['type'] = '**none';
} else {
$file['type'] = Horde_String::lower($name[count($name) - 1]);
}
$file['size'] = $line['length'];
} elseif ($line['vfs_type'] == self::FOLDER) {
$file['type'] = '**dir';
$file['size'] = -1;
}
$file['date'] = $line['vfs_modified'];
$file['owner'] = isset($line['vfs_owner']) ? $line['vfs_owner'] : '';
$file['perms'] = '';
$file['group'] = '';
// filtering
if ($this->_filterMatch($filter, $file['name'])) {
unset($file);
continue;
}
if ($dironly && $file['type'] !== '**dir') {
unset($file);
continue;
}
$files[$file['name']] = $file;
unset($file);
}
return $files;
}
/**
* Garbage collect files in the VFS storage system.
*
* @param string $path The VFS path to clean.
* @param integer $secs The minimum amount of time (in seconds) required
* before a file is removed.
*
* @throws Horde_Vfs_Exception
*/
public function gc($path, $secs = 345600)
{
$path = $this->_convertPath($path);
list($op, $values) = $this->_nullString($path);
$sql = sprintf(
'DELETE FROM %s WHERE vfs_type = ? AND vfs_modified < ? AND (vfs_path %s OR vfs_path LIKE ?)',
$this->_params['table'],
$op
);
$values = array_merge(
array(self::FILE, time() - $secs),
$values,
array($path . '/%')
);
try {
$this->_db->delete($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
}
/**
* Renames all child paths.
*
* @param string $path The path of the folder to rename.
* @param string $name The foldername to use.
*
* @throws Horde_Vfs_Exception
*/
protected function _recursiveRename($oldpath, $oldname, $newpath, $newname)
{
$oldpath = $this->_convertPath($oldpath);
$newpath = $this->_convertPath($newpath);
$sql = sprintf(
'SELECT vfs_name FROM %s WHERE vfs_type = ? AND vfs_path = ?',
$this->_params['table']
);
$values = array(self::FOLDER, $this->_getNativePath($oldpath, $oldname));
try {
$folderList = $this->_db->selectValues($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
foreach ($folderList as $folder) {
$this->_recursiveRename($this->_getNativePath($oldpath, $oldname), $folder, $this->_getNativePath($newpath, $newname), $folder);
}
$sql = sprintf(
'UPDATE %s SET vfs_path = ? WHERE vfs_path = ?',
$this->_params['table']
);
$values = array(
$this->_getNativePath($newpath, $newname),
$this->_getNativePath($oldpath, $oldname)
);
try {
$this->_db->update($sql, $values);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
}
/**
* Return a full filename on the native filesystem, from a VFS
* path and name.
*
* @param string $path The VFS file path.
* @param string $name The VFS filename.
*
* @return string The full native filename.
*/
protected function _getNativePath($path, $name)
{
if (!strlen($path)) {
return $name;
}
if (isset($this->_params['home']) &&
preg_match('|^~/?(.*)$|', $path, $matches)) {
$path = $this->_params['home'] . '/' . $matches[1];
}
return $path . '/' . $name;
}
/**
* Read file data from the SQL VFS backend.
*
* @param string $table The VFS table name.
* @param string $field TODO
* @param array $criteria TODO
*
* @return mixed TODO
* @throws Horde_Vfs_Exception
*/
protected function _readBlob($table, $field, $criteria)
{
if (!count($criteria)) {
throw new Horde_Vfs_Exception('You must specify the fetch criteria');
}
$where = '';
$values = array();
foreach ($criteria as $key => $value) {
if (!empty($where)) {
$where .= ' AND ';
}
list($op, $val) = $this->_nullString($value);
$where .= $key . ' ' . $op;
$values = array_merge($values, $val);
}
$sql = sprintf(
'SELECT %s FROM %s WHERE %s',
$field, $table, $where
);
try {
$result = $this->_db->selectValue($sql, $values);
$columns = $this->_db->columns($table);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
if ($result === false) {
throw new Horde_Vfs_Exception('Unable to load SQL data.');
}
return $columns[$field]->binaryToString($result);
}
/**
* TODO
*
* @param string $table TODO
* @param string $field TODO
* @param string $data TODO
* @param string $attributes TODO
*
* @return mixed TODO
* @throws Horde_Vfs_Exception
*/
protected function _insertBlob($table, $field, $data, $attributes)
{
$attributes[$field] = new Horde_Db_Value_Binary($data);
/* Execute the query. */
try {
$this->_db->insertBlob($table, $attributes);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
}
/**
* TODO
*
* @param string $table TODO
* @param string $field TODO
* @param string $data TODO
* @param string $where TODO
* @param array $alsoupdate TODO
*
* @return mixed TODO
* @throws Horde_Vfs_Exception
*/
protected function _updateBlob($table, $field, $data, $where, $alsoupdate)
{
$wherestring = '';
$wherevalues = array();
foreach ($where as $key => $value) {
if (!empty($wherestring)) {
$wherestring .= ' AND ';
}
$wherestring .= $key . ' = ?';
$wherevalues[] = $value;
}
/* Execute the query. */
try {
$this->_db->updateBlob(
$table,
array_merge(
$alsoupdate,
array($field => new Horde_Db_Value_Binary($data))
),
array($wherestring, $wherevalues)
);
} catch (Horde_Db_Exception $e) {
throw new Horde_Vfs_Exception($e);
}
}
/**
* Converts the path name from regular filesystem form to the internal
* format needed to access the file in the database.
*
* Namely, we will treat '/' as a base directory as this is pretty much
* the standard way to access base directories over most filesystems.
*
* @param string $path A VFS path.
*
* @return string The path with any surrouding slashes stripped off.
*/
protected function _convertPath($path)
{
return trim($path, '/');
}
/**
* TODO
*
* @return string TODO
*/
protected function _getFileSizeOp()
{
switch ($this->_db->adapterName()) {
case 'PostgreSQL':
case 'PDO_PostgreSQL':
return 'OCTET_LENGTH';
default:
return 'LENGTH';
}
}
/**
* Returns a comparison for a possibly empty string.
*
* Returns IS NULL instead of an equals operator if the string is empty.
*
* @param string $value A string.
*
* @return array
*/
protected function _nullString($value)
{
return strlen($value)
? array('= ?', array($value))
: array('IS NULL', array());
}
}