<?php
/**
 * Horde_Vfs:: implementation using PHP's PEAR database abstraction
 * layer and local file system for file storage.
 *
 * Required values for $params:<pre>
 * db - (DB) The DB object.
 * vfsroot - (string) The root directory of where the files should be
 *           actually stored.</pre>
 *
 * Optional values:<pre>
 * table - (string) The name of the vfs table in 'database'. Defaults to
 *         'horde_vfs'.</pre>
 *
 * The table structure for the VFS can be found in data/vfs.sql.
 *
 * @author   Michael Varghese <mike.varghese@ascellatech.com>
 * @category Horde
 * @package  VFS
 */
class Horde_Vfs_SqlFile extends Horde_Vfs_File
{
    /* 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 DB
     */
    protected $_db = false;

    /**
     * Constructor.
     *
     * @param array $params  A hash containing connection parameters.
     */
    public function __construct($params = array())
    {
        throw new Horde_Vfs_Exception('The SqlFile VFS driver needs to be refactored to a real composite driver.');
    }

    /**
     * Store a file in the VFS, with the data copied from a temporary
     * file.
     *
     * @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)
    {
        /* No need to check quota here as we will check it when we call
         * writeData(). */
        if (!$data = @fopen($tmpFile, 'rb')) {
            throw new Horde_Vfs_Exception('Unable to open ' . $tmpFile);
        }
        return $this->writeData($path, $name, $data, $autocreate);
    }

    /**
     * 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 $data         The file data.
     * @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);

        $fp = @fopen($this->_getNativePath($path, $name), 'w');
        if (!$fp) {
            if ($autocreate) {
                $this->autocreatePath($path);
                $fp = @fopen($this->_getNativePath($path, $name), 'w');
                if (!$fp) {
                    throw new Horde_Vfs_Exception('Unable to open VFS file for writing.');
                }
            } else {
                throw new Horde_Vfs_Exception('Unable to open VFS file for writing.');
            }
        }
        if (is_resource($data)) {
            rewind($data);
        }

        if (!@fwrite($fp, $data)) {
            throw new Horde_Vfs_Exception('Unable to write VFS file data.');
        }

        if ($this->_writeSQLData($path, $name) instanceof PEAR_Error) {
            @unlink($this->_getNativePath($path, $name));
            throw new Horde_Vfs_Exception('Unable to write VFS file data.');
        }
    }

    /**
     * Moves a file in the database and the file system.
     *
     * @param string $path         The path to store the file in.
     * @param string $name         The old filename.
     * @param string $dest         The new filename.
     * @param boolean $autocreate  Automatically create directories?
     *
     * @throws Horde_Vfs_Exception
     */
    public function move($path, $name, $dest, $autocreate = false)
    {
        $orig = $this->_getNativePath($path, $name);
        if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
            throw new Horde_Vfs_Exception('Cannot move file(s) - destination is within source.');
        }

        if ($autocreate) {
            $this->autocreatePath($dest);
        }

        foreach ($this->listFolder($dest, null, false) as $file) {
            if ($file['name'] == $name) {
                throw new Horde_Vfs_Exception('Unable to move VFS file.');
            }
        }

        if (strpos($dest, $this->_getSQLNativePath($path, $name)) !== false) {
            throw new Horde_Vfs_Exception('Unable to move VFS file.');
        }

        $this->rename($path, $name, $dest, $name);
    }

    /**
     * Copies a file through the backend.
     *
     * @param string $path         The path to store the file in.
     * @param string $name         The filename to use.
     * @param string $dest         The destination of the file.
     * @param boolean $autocreate  Automatically create directories?
     *
     * @throws Horde_Vfs_Exception
     */
    public function copy($path, $name, $dest, $autocreate = false)
    {
        $this->_checkDestination($path, $dest);

        $this->_connect();

        if ($autocreate) {
            $this->autocreatePath($dest);
        }

        foreach ($this->listFolder($dest, null, false) as $file) {
            if ($file['name'] == $name) {
                throw new Horde_Vfs_Exception('Unable to copy VFS file.');
            }
        }

        if (strpos($dest, $this->_getSQLNativePath($path, $name)) !== false) {
            throw new Horde_Vfs_Exception('Unable to copy VFS file.');
        }

        if (is_dir($orig)) {
            return $this->_recursiveCopy($path, $name, $dest);
        }

        $this->_checkQuotaWrite('file', $orig, $dest, $name);

        if (!@copy($orig, $this->_getNativePath($dest, $name))) {
            throw new Horde_Vfs_Exception('Unable to copy VFS file.');
        }

        $id = $this->_db->nextId($this->_params['table']);

        $query = sprintf('INSERT INTO %s (vfs_id, vfs_type, vfs_path, vfs_name, vfs_modified, vfs_owner) VALUES (?, ?, ?, ?, ?, ?)',
                         $this->_params['table']);
        $values = array($id, self::FILE, $dest, $name, time(), $this->_params['user']);

        $result = $this->_db->query($query, $values);

        if ($result instanceof PEAR_Error) {
            unlink($this->_getNativePath($dest, $name));
            throw new Horde_Vfs_Exception($result->getMessage());
        }
    }

    /**
     * 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)
    {
        $this->_connect();

        $id = $this->_db->nextId($this->_params['table']);
        $result = $this->_db->query(sprintf('INSERT INTO %s (vfs_id, vfs_type, vfs_path, vfs_name, vfs_modified, vfs_owner)
                                            VALUES (?, ?, ?, ?, ?, ?)',
                                            $this->_params['table']),
                                    array($id, self::FOLDER, $path, $name, time(), $this->_params['user']));
        if ($result instanceof PEAR_Error) {
            throw new Horde_Vfs_Exception($result->getMessage());
        }

        if (!@mkdir($this->_getNativePath($path, $name))) {
            $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_id = ?',
                                                $this->_params['table']),
                                        array($id));
            throw new Horde_Vfs_Exception('Unable to create VFS directory.');
        }
    }

    /**
     * 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)
    {
        $this->_connect();

        if (strpos($newpath, '/') === false) {
            $parent = '';
            $path = $newpath;
        } else {
            list($parent, $path) = explode('/', $newpath, 2);
        }

        if (!$this->isFolder($parent, $path)) {
            $this->autocreatePath($newpath);
        }

        $this->_db->query(sprintf('UPDATE %s SET vfs_path = ?, vfs_name = ?, vfs_modified = ? WHERE vfs_path = ? AND vfs_name = ?', $this->_params['table']), array($newpath, $newname, time(), $oldpath, $oldname));

        if ($this->_db->affectedRows() == 0) {
            throw new Horde_Vfs_Exception('Unable to rename VFS file.');
        }

        if (is_a($this->_recursiveSQLRename($oldpath, $oldname, $newpath, $newname), 'PEAR_Error')) {
            $this->_db->query(sprintf('UPDATE %s SET vfs_path = ?, vfs_name = ?  WHERE vfs_path = ? AND vfs_name = ?', $this->_params['table']), array($oldpath, $oldname, $newpath, $newname));
            throw new Horde_Vfs_Exception('Unable to rename VFS directory.');
        }

        if (!@is_dir($this->_getNativePath($newpath))) {
            $this->autocreatePath($newpath);
        }

        if (!@rename($this->_getNativePath($oldpath, $oldname), $this->_getNativePath($newpath, $newname))) {
            $this->_db->query(sprintf('UPDATE %s SET vfs_path = ?, vfs_name = ? WHERE vfs_path = ? AND vfs_name = ?', $this->_params['table']), array($oldpath, $oldname, $newpath, $newname));
            return PEAR::raiseError(Horde_Vfs_Translation::t("Unable to rename VFS file."));
        }
    }

    /**
     * Delete a folder from the VFS.
     *
     * @param string $path        The path to delete the folder from.
     * @param string $name        The foldername to use.
     * @param boolean $recursive  Force a recursive delete?
     *
     * @throws Horde_Vfs_Exception
     */
    public function deleteFolder($path, $name, $recursive = false)
    {
        $this->_connect();

        if ($recursive) {
            $this->emptyFolder($path . '/' . $name);
        } else {
            $list = $this->listFolder($path . '/' . $name);
            if (count($list)) {
                throw new Horde_Vfs_Exception(sprintf('Unable to delete %s, the directory is not empty', $path . '/' . $name));
            }
        }

        $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_path = ? AND vfs_name = ?', $this->_params['table']), array(self::FOLDER, $path, $name));

        if ($this->_db->affectedRows() == 0 || ($result instanceof PEAR_Error)) {
            throw new Horde_Vfs_Exception('Unable to delete VFS directory.');
        }

        if ($this->_recursiveSQLDelete($path, $name) instanceof PEAR_Error ||
            $this->_recursiveLFSDelete($path, $name) instanceof PEAR_Error) {
            throw new Horde_Vfs_Exception('Unable to delete VFS directory recursively.');
        }
    }

    /**
     * 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);
        $this->_connect();

        $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_path = ? AND vfs_name = ?',
                                            $this->_params['table']),
                                    array(self::FILE, $path, $name));

        if ($this->_db->affectedRows() == 0) {
            throw new Horde_Vfs_Exception('Unable to delete VFS file.');
        }

        if ($result instanceof PEAR_Error) {
            throw new Horde_Vfs_Exception($result->getMessage());
        }

        if (!@unlink($this->_getNativePath($path, $name))) {
            throw new Horde_Vfs_Exception('Unable to delete VFS file.');
        }
    }

    /**
     * Returns 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)
    {
        $this->_connect();

        $files = array();

        $fileList = $this->_db->getAll(sprintf('SELECT vfs_name, vfs_type, vfs_modified, vfs_owner FROM %s
                                               WHERE vfs_path = ?',
                                               $this->_params['table']),
                                       array($path));
        if ($fileList instanceof PEAR_Error) {
            throw new Horde_Vfs_Exception($fileList->getMessage());
        }

        foreach ($fileList as $line) {
            // Filter out dotfiles if they aren't wanted.
            if (!$dotfiles && substr($line[0], 0, 1) == '.') {
                continue;
            }

            $file['name'] = $line[0];

            if ($line[1] == self::FILE) {
                $name = explode('.', $line[0]);

                if (count($name) == 1) {
                    $file['type'] = '**none';
                } else {
                    $file['type'] = Horde_String::lower($name[count($name) - 1]);
                }

                $file['size'] = filesize($this->_getNativePath($path, $line[0]));
            } elseif ($line[1] == self::FOLDER) {
                $file['type'] = '**dir';
                $file['size'] = -1;
            }

            $file['date'] = $line[2];
            $file['owner'] = $line[3];
            $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;
    }

    /**
     * Recursively copies the contents of a folder to a destination.
     *
     * @param string $path  The path to store the directory in.
     * @param string $name  The name of the directory.
     * @param string $dest  The destination of the directory.
     *
     * @throws Horde_Vfs_Exception
     */
    protected function _recursiveCopy($path, $name, $dest)
    {
        $this->createFolder($dest, $name);

        $file_list = $this->listFolder($this->_getSQLNativePath($path, $name));
        foreach ($file_list as $file) {
            $this->copy($this->_getSQLNativePath($path, $name), $file['name'], $this->_getSQLNativePath($dest, $name));
        }
    }

    /**
     * Store a files information within the database.
     *
     * @param string $path         The path to store the file in.
     * @param string $name         The filename to use.
     *
     * @throws Horde_Vfs_Exception
     */
    protected function _writeSQLData($path, $name)
    {
        $this->_connect();

        // File already exists in database
        if ($this->exists($path, $name)) {
            $query = 'UPDATE ' . $this->_params['table'] .
                     ' SET vfs_modified = ?' .
                     ' WHERE vfs_path = ? AND vfs_name = ?';
            $values = array(time(), $path, $name);
        } else {
            $id = $this->_db->nextId($this->_params['table']);

            $query = 'INSERT INTO ' . $this->_params['table'] .
                     ' (vfs_id, vfs_type, vfs_path, vfs_name, vfs_modified,' .
                     ' vfs_owner) VALUES (?, ?, ?, ?, ?, ?)';
            $values = array($id, self::FILE, $path, $name, time(),
                            $this->_params['user']);
        }
        return $this->_db->query($query, $values);
    }

    /**
     * Renames all child paths.
     *
     * @param string $oldpath  The old path of the folder to rename.
     * @param string $oldname  The old name.
     * @param string $newpath  The new path of the folder to rename.
     * @param string $newname  The new name.
     *
     * @throws Horde_Vfs_Exception
     */
    protected function _recursiveSQLRename($oldpath, $oldname, $newpath,
                                           $newname)
    {
        $folderList = $this->_db->getCol(sprintf('SELECT vfs_name FROM %s WHERE vfs_type = ? AND vfs_path = ?',
                                                 $this->_params['table']),
                                         0,
                                         array(self::FOLDER, $this->_getSQLNativePath($oldpath, $oldname)));

        foreach ($folderList as $folder) {
            $this->_recursiveSQLRename($this->_getSQLNativePath($oldpath, $oldname), $folder, $this->_getSQLNativePath($newpath, $newname), $folder);
        }

        $result = $this->_db->query(sprintf('UPDATE %s SET vfs_path = ? WHERE vfs_path = ?',
                                            $this->_params['table']),
                                    array($this->_getSQLNativePath($newpath, $newname),
                                          $this->_getSQLNativePath($oldpath, $oldname)));

        if ($result instanceof PEAR_Error) {
            throw new Horde_Vfs_Exception($result->getMessage());
        }
    }

    /**
     * Delete a folders contents from the VFS in the SQL database,
     * recursively.
     *
     * @param string $path  The path of the folder.
     * @param string $name  The foldername to use.
     *
     * @throws Horde_Vfs_Exception
     */
    protected function _recursiveSQLDelete($path, $name)
    {
        $result = $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_path = ?', $this->_params['table']), array(self::FILE, $this->_getSQLNativePath($path, $name)));
        if ($result instanceof PEAR_Error) {
            throw new Horde_Vfs_Exception($result->getMessage());
        }

        $folderList = $this->_db->getCol(sprintf('SELECT vfs_name FROM %s WHERE vfs_type = ? AND vfs_path = ?', $this->_params['table']), 0, array(self::FOLDER, $this->_getSQLNativePath($path, $name)));

        foreach ($folderList as $folder) {
            $this->_recursiveSQLDelete($this->_getSQLNativePath($path, $name), $folder);
        }

        $this->_db->query(sprintf('DELETE FROM %s WHERE vfs_type = ? AND vfs_name = ? AND vfs_path = ?', $this->_params['table']), array(self::FOLDER, $name, $path));
    }

    /**
     * Delete a folders contents from the VFS, recursively.
     *
     * @param string $path  The path of the folder.
     * @param string $name  The foldername to use.
     *
     * @throws Horde_Vfs_Exception
     */
    protected function _recursiveLFSDelete($path, $name)
    {
        $dir = $this->_getNativePath($path, $name);
        $dh = @opendir($dir);

        while (false !== ($file = readdir($dh))) {
            if ($file != '.' && $file != '..') {
                if (is_dir($dir . '/' . $file)) {
                    $this->_recursiveLFSDelete(!strlen($path) ? $name : $path . '/' . $name, $file);
                } else {
                    @unlink($dir . '/' . $file);
                }
            }
        }
        @closedir($dh);

        rmdir($dir);
    }

    /**
     * Attempts to open a persistent connection to the SQL server.
     *
     * @throws Horde_Vfs_Exception
     */
    protected function _connect()
    {
        if ($this->_db !== false) {
            return;
        }

        $required = array('db', 'vfsroot');
        foreach ($required as $val) {
            if (!isset($this->_params[$val])) {
                throw new Horde_Vfs_Exception(sprintf('Required "%s" not specified in VFS configuration.', $val));
            }
        }

        $this->_params = array_merge(array(
            'table' => 'horde_vfs',
        ), $this->_params);

        $this->_db = $this->_params['db'];
    }

    /**
     * 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($name)) {
            $name = '/' . $name;
        }

        if (strlen($path)) {
            if (isset($this->_params['home']) &&
                preg_match('|^~/?(.*)$|', $path, $matches)) {
                $path = $this->_params['home']  . '/' . $matches[1];
            }

            return $this->_params['vfsroot'] . '/' . $path . $name;
        }

        return $this->_params['vfsroot'] . $name;
    }

    /**
     * Return a full SQL 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 _getSQLNativePath($path, $name)
    {
        return strlen($path)
            ? $path . '/' . $name
            : $name;
    }

}
