Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117889866
D4585.1775362165.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
43 KB
Referenced Files
None
Subscribers
None
D4585.1775362165.diff
View Options
diff --git a/lib/drivers/kolabfiles/kolabfiles_file_storage.php b/lib/drivers/kolabfiles/kolabfiles_file_storage.php
new file mode 100644
--- /dev/null
+++ b/lib/drivers/kolabfiles/kolabfiles_file_storage.php
@@ -0,0 +1,1312 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2024, Apheleia It |
+ | |
+ | This program is free software: you can redistribute it and/or modify |
+ | it under the terms of the GNU Affero General Public License as published |
+ | by the Free Software Foundation, either version 3 of the License, or |
+ | (at your option) any later version. |
+ | |
+ | This program 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 program. If not, see <http://www.gnu.org/licenses/> |
+ +--------------------------------------------------------------------------+
+ | Author: Christian Mollekopf <mollekopf@apheleia-it.ch> |
+ +--------------------------------------------------------------------------+
+*/
+
+// TODO
+// * Collection ACL's
+// * Support non-toplevel folders for collection creation/moves/...
+// * Paging (we don't display beyond the first page (100 items))
+// * Item modification times
+// * Searching is not implementedkj
+class kolabfiles_file_storage implements file_storage
+{
+ /**
+ * @var rcube
+ */
+ protected $rc;
+
+ protected $client = null;
+
+ /**
+ * @var array
+ */
+ protected $config = array();
+
+ /**
+ * @var seafile_api
+ */
+ protected $api;
+
+ /**
+ * List of SeaFile libraries
+ *
+ * @var array
+ */
+ protected $libraries;
+
+ /**
+ * Instance title (mount point)
+ *
+ * @var string
+ */
+ protected $title;
+
+
+ /**
+ * Class constructor
+ */
+ public function __construct()
+ {
+ $this->rc = rcube::get_instance();
+ $this->config = [
+ 'baseuri' => $this->rc->config->get('fileapi_kolabfiles_baseuri', 'https://' . $_SERVER["HTTP_HOST"] . '/api/'),
+ 'host' => $this->rc->config->get('fileapi_kolabfiles_host', $_SERVER["HTTP_HOST"]),
+ ];
+ }
+
+
+ protected function init()
+ {
+ if ($this->client !== null) {
+ return true;
+ }
+
+ $stack = new \GuzzleHttp\HandlerStack();
+ $stack->setHandler(\GuzzleHttp\choose_handler());
+ $stack->push(\GuzzleHttp\Middleware::retry(
+ function (
+ $retries,
+ \GuzzleHttp\Psr7\Request $request,
+ \GuzzleHttp\Psr7\Response $response = null,
+ \GuzzleHttp\Exception\RequestException $exception = null
+ ) {
+ $maxRetries = 2;
+
+ if ($retries >= $maxRetries) {
+ return false;
+ }
+
+ if ($response && $response->getStatusCode() === 401) {
+ $this->refreshClientAccessToken();
+ return true;
+ }
+
+ return false;
+ }
+ ));
+
+ $stack->push(\GuzzleHttp\Middleware::mapRequest(function (\GuzzleHttp\Psr7\Request $request) {
+ //TODO Just forward the bearer token, once we manage to make sure it get's sent to roundcube
+ // 'Authorization' => rcube_utils::request_header('Authorization')
+ if ($accessToken = $this->getClientAccessToken()) {
+ return $request->withHeader('Authorization', 'Bearer ' . $accessToken);
+ }
+ return $request;
+ }));
+
+ $this->client = new \GuzzleHttp\Client(
+ [
+ 'http_errors' => false, // No exceptions from Guzzle
+ 'base_uri' => rtrim($this->config['baseuri'], '/') . '/',
+ 'handler' => $stack,
+ 'verify' => false,
+ 'connect_timeout' => 10,
+ 'timeout' => 10,
+ // 'on_stats' => function (\GuzzleHttp\TransferStats $stats) {
+ // $threshold = \config('logging.slow_log');
+ // if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) {
+ // $url = $stats->getEffectiveUri();
+ // $method = $stats->getRequest()->getMethod();
+ // \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec));
+ // }
+ // },
+ ]
+ );
+ }
+
+ private function getClientAccessToken()
+ {
+ if (!empty($_SESSION['access_token'])) {
+ return $this->rc->decrypt($_SESSION['access_token']);
+ }
+ return null;
+ }
+
+ private function refreshClientAccessToken()
+ {
+ //TODO use the refresh token if available instead of refreshing from scratch always.
+ rcube::write_log('kolabfiles', "Refreshing the access token");
+ $username = $_SESSION['username'];
+ $password = $this->rc->decrypt($_SESSION['password']);
+
+ $client = new \GuzzleHttp\Client([
+ 'http_errors' => false, // No exceptions from Guzzle
+ 'base_uri' => rtrim($this->config['baseuri'], '/') . '/',
+ 'verify' => false,
+ 'connect_timeout' => 10,
+ 'timeout' => 10,
+ ]);
+
+ $response = $client->request('POST', "auth/login?email=$username&password=$password");
+ if ($response->getStatusCode() != 200) {
+ throw new Exception("Failed to authenticate $username:$password");
+ }
+ $json = json_decode($response->getBody(), true);
+ $accessToken = $json['access_token'];
+ $_SESSION['access_token'] = $this->rc->encrypt($accessToken);
+ }
+
+ protected function client()
+ {
+ $this->init();
+ return $this->client;
+ }
+
+ /**
+ * Authenticates a user
+ *
+ * @param string $username User name
+ * @param string $password User password
+ *
+ * @param bool True on success, False on failure
+ */
+ public function authenticate($username, $password)
+ {
+ $_SESSION['username'] = $username;
+ $_SESSION['password'] = $this->rc->encrypt($password);
+
+
+ $this->init();
+
+ return true;
+ }
+
+ /**
+ * Get password and name of authenticated user
+ *
+ * @return array Authenticated user data
+ */
+ public function auth_info()
+ {
+ return array(
+ 'username' => $_SESSION['username'],
+ 'password' => $this->rc->decrypt($_SESSION['password']),
+ );
+ }
+
+ /**
+ * Configures environment
+ *
+ * @param array $config Configuration
+ * @param string $title Source identifier
+ */
+ public function configure($config, $title = null)
+ {
+ $this->config = array_merge($this->config, $config);
+ $this->title = $title;
+ }
+
+ /**
+ * Returns current instance title
+ *
+ * @return string Instance title (mount point)
+ */
+ public function title()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Storage driver capabilities
+ *
+ * @return array List of capabilities
+ */
+ public function capabilities()
+ {
+ // find max filesize value
+ $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
+ $max_postsize = parse_bytes(ini_get('post_max_size'));
+ if ($max_postsize && $max_postsize < $max_filesize) {
+ $max_filesize = $max_postsize;
+ }
+
+ return array(
+ file_storage::CAPS_MAX_UPLOAD => $max_filesize,
+ file_storage::CAPS_QUOTA => true,
+ file_storage::CAPS_LOCKS => true,
+ file_storage::CAPS_ACL => true,
+ );
+ }
+
+ /**
+ * Save configuration of external driver (mount point)
+ *
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_create($driver)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Delete configuration of external driver (mount point)
+ *
+ * @param string $title Driver instance name
+ *
+ * @throws Exception
+ */
+ public function driver_delete($title)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Return list of registered drivers (mount points)
+ *
+ * @return array List of drivers data
+ * @throws Exception
+ */
+ public function driver_list()
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Update configuration of external driver (mount point)
+ *
+ * @param string $title Driver instance name
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_update($title, $driver)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Returns metadata of the driver
+ *
+ * @return array Driver meta data (image, name, form)
+ */
+ public function driver_metadata()
+ {
+ $image_content = file_get_contents(__DIR__ . '/kolab.png');
+
+ $metadata = array(
+ 'image' => 'data:image/png;base64,' . base64_encode($image_content),
+ 'name' => 'Kolab Files',
+ 'ref' => 'http://kolab.org',
+ 'description' => 'Kolab File Storage',
+ 'form' => array(
+ 'host' => 'hostname',
+ 'username' => 'username',
+ 'password' => 'password',
+ ),
+ );
+
+ return $metadata;
+ }
+
+ /**
+ * Validate metadata (config) of the driver
+ *
+ * @param array $metadata Driver metadata
+ *
+ * @return array Driver meta data to be stored in configuration
+ * @throws Exception
+ */
+ public function driver_validate($metadata)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Create a file.
+ *
+ * @param string $file_name Name of a file (with folder path)
+ * @param array $file File data (path, type)
+ *
+ * @throws Exception
+ */
+ public function file_create($file_name, $file)
+ {
+ list($fn, $repo_id) = $this->find_library($file_name);
+
+ if (empty($repo_id)) {
+ throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
+ }
+
+ $data = null;
+ $fp = null;
+ if ($file['path']) {
+ //FIXME do we need to fclose again? it somehow fails, so I'm not sure.
+ $fp = fopen($file['path'], 'r');
+ $data = $fp;
+ } else {
+ $data = $file['content'];
+ }
+
+ $response = $this->client->request("POST", "v4/fs?name=$fn&parent=$repo_id", ['body' => $data]);
+ $created = $response->getStatusCode() == 200;
+
+ if (!$created) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving file to SeaFile server"),
+ true, false);
+
+ throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Update a file.
+ *
+ * @param string $file_name Name of a file (with folder path)
+ * @param array $file File data (path, type)
+ *
+ * @throws Exception
+ */
+ public function file_update($file_name, $file)
+ {
+ list($fn, $repo_id) = $this->find_library($file_name);
+ $file_id = $this->find_file_id($fn, $repo_id);
+
+ if (empty($repo_id)) {
+ throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
+ }
+
+ $data = null;
+ $fp = null;
+ if ($file['path']) {
+ //FIXME do we need to fclose again? it somehow fails, so I'm not sure.
+ $fp = fopen($file['path'], 'r');
+ $data = $fp;
+ } else {
+ $data = $file['content'];
+ }
+
+ $response = $this->client->request("PATCH", "v4/fs/$file_id?media=content", ['body' => $data]);
+
+ if ($response->getStatusCode() != 200) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving file to SeaFile server"),
+ true, false);
+
+ throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Delete a file.
+ *
+ * @param string $file_name Name of a file (with folder path)
+ *
+ * @throws Exception
+ */
+ public function file_delete($file_name)
+ {
+ list($file_name, $repo_id) = $this->find_library($file_name);
+
+ if ($repo_id && $file_name != '/') {
+ $file_id = $this->find_file_id($file_name, $repo_id);
+ $response = $this->client->request("DELETE", "v4/fs/$file_id");
+ $deleted = $response->getStatusCode() == 200;
+ }
+
+ if (!$deleted) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error deleting object from SeaFile server"),
+ true, false);
+
+ throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Return file body.
+ *
+ * @param string $file_name Name of a file (with folder path)
+ * @param array $params Parameters (force-download, force-type, head)
+ * @param resource $fp Print to file pointer instead (send no headers)
+ *
+ * @throws Exception
+ */
+ public function file_get($file_name, $params = array(), $fp = null)
+ {
+ list($fn, $repo_id) = $this->find_library($file_name);
+ $file_id = $this->find_file_id($fn, $repo_id);
+
+ // $file = $this->from_file_object($file);
+ $response = $this->client->request("GET", "v4/fs/{$file_id}");
+ $file = json_decode($response->getBody(), true);
+
+ // write to file pointer, send no headers
+ if ($fp) {
+ $response = $this->client->request("GET", "v4/fs/{$file_id}?download=1");
+ fwrite(fp, $request->getBody());
+
+ return;
+ }
+
+ if (!empty($params['force-download'])) {
+ $disposition = 'attachment';
+ header("Content-Type: application/octet-stream");
+// @TODO
+// if ($browser->ie)
+// header("Content-Type: application/force-download");
+ }
+ else {
+ $mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['mimetype']);
+ $disposition = 'inline';
+
+ header("Content-Transfer-Encoding: binary");
+ header("Content-Type: $mimetype");
+ }
+
+ $filename = addcslashes($file['name'], '"');
+
+ // Workaround for nasty IE bug (#1488844)
+ // If Content-Disposition header contains string "attachment" e.g. in filename
+ // IE handles data as attachment not inline
+/*
+@TODO
+ if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) {
+ $filename = str_ireplace('attachment', 'attach', $filename);
+ }
+*/
+ header("Content-Length: " . $file['size']);
+ header("Content-Disposition: $disposition; filename=\"$filename\"");
+
+ // just send redirect to SeaFile server
+ if ($file['size'] && empty($params['head'])) {
+ $allow_redirects = $this->rc->config->get('fileapi_seafile_allow_redirects');
+ // In view-mode we can't redirect to SeaFile server because:
+ // - it responds with Content-Disposition: attachment, which causes that
+ // e.g. previewing images is not possible
+ // - pdf/odf viewers can't follow redirects for some reason (#4590)
+ if ($allow_redirects && !empty($params['force-download'])) {
+ $response = $this->client->request("GET", "v4/fs/{$file_id}?downloadUrl=1");
+ $json = json_decode($response->getBody(), true);
+ $link = $json['downloadUrl'];
+ header("Location: $link");
+ }
+ else if ($fp = fopen('php://output', 'wb')) {
+ $response = $this->client->request("GET", "v4/fs/{$file_id}?download=1");
+ fwrite($fp, $response->getBody());
+ fclose($fp);
+ }
+ }
+ }
+
+ /**
+ * Returns file metadata.
+ *
+ * @param string $file_name Name of a file (with folder path)
+ *
+ * @throws Exception
+ */
+ public function file_info($file_name)
+ {
+ list($file, $repo_id) = $this->find_library($file_name);
+
+ $file_id = $this->find_file_id($file, $repo_id);
+ //TODO find the file id
+
+ $response = $this->client->request("GET", "v4/fs/{$file_id}");
+ $json = json_decode($response->getBody(), true);
+
+
+
+ if (empty($json)) {
+ throw new Exception("Storage error. File not found.", file_storage::ERROR);
+ }
+
+ $file = $this->from_file_object($json);
+
+ //FIXME modification times are missing from the source
+ return array(
+ 'name' => $file['name'],
+ 'size' => (int) $file['size'],
+ 'type' => (string) $file['type'],
+ 'mtime' => file_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']),
+ 'ctime' => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']),
+ 'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
+ 'created' => $file['created'] ? $file['created']->format('U') : 0,
+ );
+ }
+
+ /**
+ * List files in a folder.
+ *
+ * @param string $folder_name Name of a folder with full path
+ * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
+ *
+ * @return array List of files (file properties array indexed by filename)
+ * @throws Exception
+ */
+ public function file_list($folder_name, $params = array())
+ {
+ // mount point contains only folders
+ if (!is_string($folder_name) || $folder_name === '') {
+ return array();
+ }
+
+ list($folder, $repo_id) = $this->find_library($folder_name);
+
+ // prepare search filter
+ if (!empty($params['search'])) {
+ foreach ($params['search'] as $idx => $value) {
+ if ($idx == 'name') {
+ $params['search'][$idx] = mb_strtoupper($value);
+ }
+ else if ($idx == 'class') {
+ $params['search'][$idx] = file_utils::class2mimetypes($value);
+ }
+ }
+ }
+
+ // get directory entries
+ // $entries = $this->api->directory_entries($repo_id, $folder, 'file');
+ // $response = $this->actingAs($john)->get("fs?parent={$collection->id}");
+
+ $response = $this->client()->request('GET', "v4/fs?parent={$repo_id}&type=file");
+ $json = json_decode($response->getBody(), true);
+ $entries = $json['list'];
+
+
+ $result = array();
+
+ foreach ((array) $entries as $idx => $file) {
+
+
+ //TODO file['name']
+ //TODO file['type'] = 'file'
+ //TODO file['type']
+
+ if ($file['type'] != 'file') {
+ continue;
+ }
+
+ $file_id = $file['id'];
+ $file = $this->from_file_object($file);
+
+ //FIXME slightly wasteful to just get the size....
+ $response = $this->client->request("GET", "v4/fs/{$file_id}");
+ $json = json_decode($response->getBody(), true);
+ $file['size'] = $json['size'];
+
+ // search filter
+ if (!empty($params['search'])) {
+ foreach ($params['search'] as $idx => $value) {
+ if ($idx == 'name') {
+ if (strpos(mb_strtoupper($file['name']), $value) === false) {
+ continue 2;
+ }
+ }
+ else if ($idx == 'class') {
+ foreach ($value as $v) {
+ if (stripos($file['type'], $v) !== false) {
+ continue 2;
+ }
+ }
+
+ continue 2;
+ }
+ }
+ }
+
+ $filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name'];
+
+ $result[$filename] = array(
+ 'name' => $file['name'],
+ 'size' => (int) $file['size'],
+ 'type' => (string) $file['type'],
+ //FIXME we don't have the changed date
+ 'mtime' => file_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']),
+ //FIXME we don't have the created date date
+ 'ctime' => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']),
+ //FIXME we don't have the created date date
+ 'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
+ //FIXME we don't have the created date date
+ 'created' => $file['created'] ? $file['created']->format('U') : 0,
+ );
+
+ unset($entries[$idx]);
+ }
+
+ // @TODO: pagination, search (by filename, mimetype)
+
+ // Sorting
+ $sort = !empty($params['sort']) ? $params['sort'] : 'name';
+ $index = array();
+
+ if ($sort == 'mtime') {
+ $sort = 'modified';
+ }
+
+ if (in_array($sort, array('name', 'size', 'modified'))) {
+ foreach ($result as $key => $val) {
+ $index[$key] = $val[$sort];
+ }
+ array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
+ }
+
+ if ($params['reverse']) {
+ $result = array_reverse($result, true);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Copy a file.
+ *
+ * @param string $file_name Name of a file (with folder path)
+ * @param string $new_name New name of a file (with folder path)
+ *
+ * @throws Exception
+ */
+ public function file_copy($file_name, $new_name)
+ {
+ list($src_name, $repo_id) = $this->find_library($file_name);
+ list($dst_name, $dst_repo_id) = $this->find_library($new_name);
+
+ if ($repo_id && $dst_repo_id) {
+ $path_src = explode('/', $src_name);
+ $path_dst = explode('/', $dst_name);
+ $f_src = array_pop($path_src);
+ $f_dst = array_pop($path_dst);
+ $src_dir = '/' . ltrim(implode('/', $path_src), '/');
+ $dst_dir = '/' . ltrim(implode('/', $path_dst), '/');
+
+ //FIXME
+ $success = $this->api->file_copy($repo_id, $f_src, $src_dir, $dst_dir, $dst_repo_id);
+
+ // now rename the file if needed
+ if ($success && $f_src != $f_dst) {
+ $success = $this->api->file_rename($dst_repo_id, rtrim($dst_dir, '/') . '/' . $f_src, $f_dst);
+ }
+ }
+
+ if (!$success) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error copying file on SeaFile server"),
+ true, false);
+
+ throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Move (or rename) a file.
+ *
+ * @param string $file_name Name of a file (with folder path)
+ * @param string $new_name New name of a file (with folder path)
+ *
+ * @throws Exception
+ */
+ public function file_move($file_name, $new_name)
+ {
+ list($src_name, $repo_id) = $this->find_library($file_name);
+ list($dst_name, $dst_repo_id) = $this->find_library($new_name);
+ $file_id = $this->find_file_id($src_name, $repo_id);
+
+ if ($repo_id && $dst_repo_id) {
+ $path_src = explode('/', $src_name);
+ $path_dst = explode('/', $dst_name);
+ $f_src = array_pop($path_src);
+ $f_dst = array_pop($path_dst);
+ $src_dir = '/' . ltrim(implode('/', $path_src), '/');
+ $dst_dir = '/' . ltrim(implode('/', $path_dst), '/');
+
+ if ($src_dir == $dst_dir && $repo_id == $dst_repo_id) {
+ $success = true;
+ }
+ else {
+ rcube::write_log('kolabfiles', "moving to collection $dst_repo_id");
+ //FIXME this doesn't seem to work
+ $response = $this->client->request("PUT", "v4/fs/$file_id", ['headers' => ["X-Kolab-Parents" => $dst_repo_id]]);
+ $success = $response->getStatusCode() == 200;
+ }
+
+ // now rename the file if needed
+ if ($success && $f_src != $f_dst) {
+ $response = $this->client->request("PUT", "v4/fs/$file_id?name=$dst_name");
+ $success = $response->getStatusCode() == 200;
+ }
+ }
+
+ if (!$success) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error moving file on SeaFile server"),
+ true, false);
+
+ throw new Exception("Storage error. File rename failed.", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Create a folder.
+ *
+ * @param string $folder_name Name of a folder with full path
+ *
+ * @throws Exception on error
+ */
+ public function folder_create($folder_name)
+ {
+ list($folder, $repo_id) = $this->find_library($folder_name, true);
+
+ if (empty($repo_id)) {
+ $response = $this->client->request("POST", "v4/fs?name=$folder_name&type=collection");
+ }
+ else {
+ $response = $this->client->request("POST", "v4/fs?name=$folder&type=collection&parent={$repo_id}");
+ }
+
+ $success = $response->getStatusCode() == 200;
+
+ if (!$success) {
+ throw new Exception("Storage error. Unable to create folder", file_storage::ERROR);
+ }
+
+ // clear the cache
+ if (empty($repo_id)) {
+ $this->libraries = null;
+ }
+ }
+
+ /**
+ * Delete a folder.
+ *
+ * @param string $folder_name Name of a folder with full path
+ *
+ * @throws Exception on error
+ */
+ public function folder_delete($folder_name)
+ {
+ list($folder, $repo_id) = $this->find_library($folder_name, true);
+
+ $response = $this->client->request("DELETE", "v4/fs/$repo_id");
+ $success = $response->getStatusCode() == 200;
+
+ if (!$success) {
+ throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Move/Rename a folder.
+ *
+ * @param string $folder_name Name of a folder with full path
+ * @param string $new_name New name of a folder with full path
+ *
+ * @throws Exception on error
+ */
+ public function folder_move($folder_name, $new_name)
+ {
+ list($folder, $repo_id, $library) = $this->find_library($folder_name, true);
+ list($dest_folder, $dest_repo_id) = $this->find_library($new_name, true);
+
+ // folders rename/move is possible only in the same library and folder
+ // @TODO: support folder move between libraries and folders
+ // @TODO: support converting library into a folder and vice-versa
+
+ // library rename
+ if ($repo_id && !$dest_repo_id && $folder == '/' && strpos($new_name, '/') === false) {
+ //FIXME
+ $success = $this->api->library_rename($repo_id, $new_name, $library['desc']);
+ }
+ // folder rename
+ else if ($folder != '/' && $dest_folder != '/' && $repo_id && $repo_id == $dest_repo_id) {
+ $path_src = explode('/', $folder);
+ $path_dst = explode('/', $dest_folder);
+ $f_src = array_pop($path_src);
+ $f_dst = array_pop($path_dst);
+ $src_dir = implode('/', $path_src);
+ $dst_dir = implode('/', $path_dst);
+
+ if ($src_dir == $dst_dir) {
+ $success = $this->api->directory_rename($repo_id, $folder, $f_dst);
+ }
+ }
+
+ if (!$success) {
+ throw new Exception("Storage error. Unable to rename/move folder", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Subscribe a folder.
+ *
+ * @param string $folder_name Name of a folder with full path
+ *
+ * @throws Exception
+ */
+ public function folder_subscribe($folder_name)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Unsubscribe a folder.
+ *
+ * @param string $folder_name Name of a folder with full path
+ *
+ * @throws Exception
+ */
+ public function folder_unsubscribe($folder_name)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Returns list of folders.
+ *
+ * @param array $params List parameters ('type', 'search', 'path', 'level')
+ *
+ * @return array List of folders
+ * @throws Exception
+ */
+ public function folder_list($params = array())
+ {
+ // $libraries = $this->libraries();
+ // $writable = ($params['type'] & file_storage::FILTER_WRITABLE) ? true : false;
+ // $prefix = (string) $params['path'];
+ // $prefix_len = strlen($prefix);
+ $folders = array();
+ $libraries = $this->libraries();
+ foreach ($libraries as $folder) {
+ $item = array('folder' => $folder['name']);
+ $item['readonly'] = false;
+
+ //TODO?
+ // 'mtime' => $library['mtime'],
+ // 'permission' => $library['permission'],
+
+ $folders[$folder['name']] = $item;
+ }
+
+
+ if (empty($params['extended'])) {
+ $folders = array_keys($folders);
+ }
+
+ // sort folders
+ usort($folders, array('file_utils', 'sort_folder_comparator'));
+
+ return $folders;
+ }
+
+ /**
+ * Check folder rights.
+ *
+ * @param string $folder_name Name of a folder with full path
+ *
+ * @return int Folder rights (sum of file_storage::ACL_*)
+ */
+ public function folder_rights($folder_name)
+ {
+ // It is not possible (yet) to assign a specified library/folder
+ // to the mount point. So, it is a "virtual" folder.
+ if (!strlen($folder_name)) {
+ return 0;
+ }
+
+ list($folder, $repo_id, $library) = $this->find_library($folder_name);
+
+ // @TODO: we should check directory permission not library
+ // However, there's no API for this, we'd need to get a list
+ // of directories of a parent folder/library
+/*
+ if (strpos($folder, '/')) {
+ // @TODO
+ }
+ else {
+ $acl = $library['permission'];
+ }
+*/
+ //FIXME we don't have permissions
+ return file_storage::ACL_READ | file_storage::ACL_WRITE;
+
+ // $acl = $library['permission'];
+ // $rights = 0;
+ // $map = array(
+ // 'r' => file_storage::ACL_READ,
+ // 'w' => file_storage::ACL_WRITE,
+ // );
+
+ // foreach ($map as $key => $value) {
+ // if (strpos($acl, $key) !== false) {
+ // $rights |= $value;
+ // }
+ // }
+
+ // return $rights;
+ }
+
+ /**
+ * Returns a list of locks
+ *
+ * This method should return all the locks for a particular URI, including
+ * locks that might be set on a parent URI.
+ *
+ * If child_locks is set to true, this method should also look for
+ * any locks in the subtree of the URI for locks.
+ *
+ * @param string $path File/folder path
+ * @param bool $child_locks Enables subtree checks
+ *
+ * @return array List of locks
+ * @throws Exception
+ */
+ public function lock_list($path, $child_locks = false)
+ {
+ $this->init_lock_db();
+
+ // convert URI to global resource string
+ $uri = $this->path2uri($path);
+
+ // get locks list
+ $list = $this->lock_db->lock_list($uri, $child_locks);
+
+ // convert back resource string into URIs
+ foreach ($list as $idx => $lock) {
+ $list[$idx]['uri'] = $this->uri2path($lock['uri']);
+ }
+
+ return $list;
+ }
+
+ /**
+ * Locks a URI
+ *
+ * @param string $path File/folder path
+ * @param array $lock Lock data
+ * - depth: 0/'infinite'
+ * - scope: 'shared'/'exclusive'
+ * - owner: string
+ * - token: string
+ * - timeout: int
+ *
+ * @throws Exception
+ */
+ public function lock($path, $lock)
+ {
+ $this->init_lock_db();
+
+ // convert URI to global resource string
+ $uri = $this->path2uri($path);
+
+ if (!$this->lock_db->lock($uri, $lock)) {
+ throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Removes a lock from a URI
+ *
+ * @param string $path File/folder path
+ * @param array $lock Lock data
+ *
+ * @throws Exception
+ */
+ public function unlock($path, $lock)
+ {
+ $this->init_lock_db();
+
+ // convert URI to global resource string
+ $uri = $this->path2uri($path);
+
+ if (!$this->lock_db->unlock($uri, $lock)) {
+ throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Return disk quota information for specified folder.
+ *
+ * @param string $folder_name Name of a folder with full path
+ *
+ * @return array Quota
+ * @throws Exception
+ */
+ public function quota($folder)
+ {
+ //FIXME
+ // if (!$this->init()) {
+ // throw new Exception("Storage error. Unable to get SeaFile account info.", file_storage::ERROR);
+ // }
+
+ // $account_info = $this->api->account_info();
+
+ // if (empty($account_info)) {
+ // throw new Exception("Storage error. Unable to get SeaFile account info.", file_storage::ERROR);
+ // }
+
+ // $quota = array(
+ // // expected values in kB
+ // 'total' => intval($account_info['total'] / 1024),
+ // 'used' => intval($account_info['usage'] / 1024),
+ // );
+
+ // return $quota;
+ }
+
+ /**
+ * Sharing interface
+ *
+ * @param string $folder_name Name of a folder with full path
+ * @param int $mode Sharing action mode
+ * @param array $args POST/GET parameters
+ *
+ * @return mixed Sharing response
+ * @throws Exception
+ */
+ public function sharing($folder, $mode, $args = array())
+ {
+ throw new Exception("Search not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * User/group search (autocompletion)
+ *
+ * @param string $search Search string
+ * @param int $mode Search mode
+ *
+ * @return array Users/Groups list
+ * @throws Exception
+ */
+ public function autocomplete($search, $mode)
+ {
+ throw new Exception("Autocomplete not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Convert file/folder path into a global URI.
+ *
+ * @param string $path File/folder path
+ *
+ * @return string URI
+ * @throws Exception
+ */
+ public function path2uri($path)
+ {
+ // Remove protocol prefix and path, we work with host only
+ $host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']);
+
+ if (!is_string($path) || !strlen($path)) {
+ $user = $_SESSION[$this->title . 'user'];
+ return 'kolabfiles://' . rawurlencode($user) . '@' . $host . '/';
+ }
+
+ list($file, $repo_id, $library) = $this->find_library($path);
+
+ //FIXME we don't currently have an owner
+ return 'kolabfiles://' . rawurlencode($library['owner']) . '@' . $host . '/' . file_utils::encode_path($path);
+ }
+
+ /**
+ * Convert global URI into file/folder path.
+ *
+ * @param string $uri URI
+ *
+ * @return string File/folder path
+ * @throws Exception
+ */
+ public function uri2path($uri)
+ {
+ if (!preg_match('|^kolabfiles://([^@]+)@([^/]+)/(.*)$|', $uri, $matches)) {
+ throw new Exception("Internal storage error. Unexpected data format. $uri", file_storage::ERROR);
+ }
+
+ $user = rawurldecode($matches[1]);
+ $host = $matches[2];
+ $path = file_utils::decode_path($matches[3]);
+ $c_host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']);
+
+ if (strlen($path)) {
+ list($file, $repo_id, $library) = $this->find_library($path, true);
+
+ if (empty($library) || $host != $c_host || $user != $library['owner']) {
+ throw new Exception("Internal storage error. Unresolvable URI.", file_storage::ERROR);
+ }
+ }
+
+ return $path;
+ }
+
+ /**
+ * Get list of libraries
+ */
+ private function libraries($parent = null, $path = null)
+ {
+ if ($this->libraries) {
+ return $this->libraries;
+ }
+ $folders = array();
+
+ //FIXME If we could just fetch all collections, we could assemble the tree after a single fetch.
+ if ($parent) {
+ $parentId = $parent['id'];
+ $response = $this->client()->request('GET', "v4/fs?parent=$parentId&type=collection");
+ } else {
+ $response = $this->client()->request('GET', "v4/fs?type=collection");
+ }
+ //FIXME should we just always throw on request errors? Probably?
+ if ($response->getStatusCode() != 200) {
+ rcube::write_log('kolabfiles', "The request failed: " . $response->getStatusCode());
+ throw new Exception("Get request was unsuccessful");
+ }
+ $json = json_decode($response->getBody(), true);
+ // rcube::write_log('console', var_export($json, true));
+ $libraries = $json['list'];
+
+ if (!$libraries) {
+ return [];
+ }
+
+ $libraries = array_map(function ($entry) use ($path) {
+ //FIXME: retrieve the actual owner from the api. (Do we need the owner though?), not sure it matters
+ $entry['owner'] = $_SESSION[$this->title . 'user'];
+ if ($path) {
+ $entry['name'] = $path . "/" . $entry['name'];
+ }
+ return $entry;
+ }, $libraries);
+
+ $tmp = $libraries;
+ foreach ($tmp as $lib) {
+ $subfolders = $this->libraries($lib, $lib['name']);
+ $libraries = array_merge($libraries, $subfolders);
+ }
+ if (!$parent) {
+ $this->libraries = $libraries;
+ }
+ return $libraries;
+ }
+
+
+ protected function find_file_id($file_name, $repo_id)
+ {
+ $response = $this->client()->request('GET', "v4/fs?parent={$repo_id}&type=file");
+ $json = json_decode($response->getBody(), true);
+ foreach ($json['list'] as $idx => $file) {
+ if ($file['name'] == $file_name) {
+ return $file['id'];
+ }
+ }
+ rcube::write_log('console', "Failed to find the file $file_name in $repo_id");
+ throw new Exception("Failed to find the file $file_name in $repo_id");
+ }
+
+ // /**
+ // * Find library ID from folder name
+ // */
+ protected function find_library($folder_name, $no_exception = false)
+ {
+ $libraries = $this->libraries();
+
+ foreach ($libraries as $lib) {
+ $path = $lib['name'] . '/';
+
+ if ($folder_name == $lib['name'] || strpos($folder_name, $path) === 0) {
+ if (empty($library) || strlen($library['name']) < strlen($lib['name'])) {
+ $library = $lib;
+ }
+ }
+ }
+
+ if (empty($library)) {
+ if (!$no_exception) {
+ throw new Exception("Storage error. Library not found.", file_storage::ERROR);
+ }
+ return array(null, null);
+ }
+ else {
+ $folder = substr($folder_name, strlen($library['name']) + 1);
+ }
+
+ return array(
+ // '/' . ($folder ? $folder : ''),
+ ($folder ? $folder : ''),
+ $library['id'],
+ $library
+ );
+ }
+
+ /**
+ * Simplify internal structure of the file object
+ */
+ protected function from_file_object($file)
+ {
+ // if ($file['type'] != 'file') {
+ // return null;
+ // }
+
+ //FIXME
+ // file modification time
+ // if ($file['mtime']) {
+ // try {
+ // $file['changed'] = new DateTime('@' . $file['mtime']);
+ // }
+ // catch (Exception $e) { }
+ // }
+
+ // We're not taking the servers filetype. The server might have octet/stream stored for a file,
+ // which will break the editor which needs some odt mimetype (it will silently fall back to downloading the file instead).
+ $file['type'] = file_utils::ext_to_type($file['name']);
+
+ unset($file['id']);
+ unset($file['mtime']);
+
+ return $file;
+ }
+
+ /**
+ * Initializes file_locks object
+ */
+ protected function init_lock_db()
+ {
+ if (!$this->lock_db) {
+ $this->lock_db = new file_locks;
+ }
+ }
+
+ /**
+ * Create display-username-with-email string
+ */
+ protected function user_label($name, $email)
+ {
+ if ($name && $name != $email) {
+ $label = "{$name} ({$email})";
+ }
+ else {
+ $label = $email;
+ }
+
+ return $label;
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Apr 5, 4:09 AM (13 h, 24 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18832294
Default Alt Text
D4585.1775362165.diff (43 KB)
Attached To
Mode
D4585: kolabfiles backend
Attached
Detach File
Event Timeline