diff options
author | Sergey Linnik <sergey.linnik@onlyoffice.com> | 2022-10-03 17:18:40 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-03 17:18:40 +0300 |
commit | 0f484a7eb23775ee82c6a41ec7e3d2c91790c774 (patch) | |
tree | 34ee134750d446d569d9ff70e43dd71c10498c30 | |
parent | e2714c268dd5d153724c6ad33195db9bf2a1c8b0 (diff) | |
parent | 878b623af63c18951a567c1a2eca5ee6033cf599 (diff) |
Merge pull request #591 from ONLYOFFICE/feature/federated-lock
Federated lock
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | controller/callbackcontroller.php | 10 | ||||
-rw-r--r-- | controller/federationcontroller.php | 22 | ||||
-rw-r--r-- | lib/Migration/Version070400Date20220929111111.php | 22 | ||||
-rw-r--r-- | lib/fileutility.php | 42 | ||||
-rw-r--r-- | lib/keymanager.php | 59 | ||||
-rw-r--r-- | lib/remoteinstance.php | 272 |
7 files changed, 321 insertions, 107 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index e2ab78e..df5ac70 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -48,6 +48,7 @@ return [ "ocs" => [ ["name" => "federation#key", "url" => "/api/v1/key", "verb" => "POST"], ["name" => "federation#keylock", "url" => "/api/v1/keylock", "verb" => "POST"], + ["name" => "federation#healthcheck", "url" => "/api/v1/healthcheck", "verb" => "GET"], ["name" => "editorapi#config", "url" => "/api/v1/config/{fileId}", "verb" => "GET"], ["name" => "sharingapi#get_shares", "url" => "/api/v1/shares/{fileId}", "verb" => "GET"], ["name" => "sharingapi#set_shares", "url" => "/api/v1/shares", "verb" => "PUT"] diff --git a/controller/callbackcontroller.php b/controller/callbackcontroller.php index a2fd341..9270794 100644 --- a/controller/callbackcontroller.php +++ b/controller/callbackcontroller.php @@ -39,7 +39,6 @@ use OCP\Lock\LockedException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; -use OCA\Files_Sharing\External\Storage as SharingExternalStorage; use OCA\Files_Versions\Versions\IVersionManager; use OCA\Onlyoffice\AppConfig; @@ -47,6 +46,7 @@ use OCA\Onlyoffice\Crypt; use OCA\Onlyoffice\DocumentService; use OCA\Onlyoffice\FileVersions; use OCA\Onlyoffice\KeyManager; +use OCA\Onlyoffice\RemoteInstance; use OCA\Onlyoffice\TemplateManager; /** @@ -529,8 +529,8 @@ class CallbackController extends Controller { $prevIsForcesave = KeyManager::wasForcesave($fileId); - if ($file->getStorage()->instanceOfStorage(SharingExternalStorage::class)) { - $isLock = KeyManager::lockFederatedKey($file, $isForcesave, null); + if (RemoteInstance::isRemoteFile($file)) { + $isLock = RemoteInstance::lockRemoteKey($file, $isForcesave, null); if ($isForcesave && !$isLock) { break; } @@ -543,9 +543,9 @@ class CallbackController extends Controller { return $file->putContent($newData); }); - if ($file->getStorage()->instanceOfStorage(SharingExternalStorage::class)) { + if (RemoteInstance::isRemoteFile($file)) { if ($isForcesave) { - KeyManager::lockFederatedKey($file, false, $isForcesave); + RemoteInstance::lockRemoteKey($file, false, $isForcesave); } } else { KeyManager::lock($fileId, false); diff --git a/controller/federationcontroller.php b/controller/federationcontroller.php index 8d68daf..27a9f64 100644 --- a/controller/federationcontroller.php +++ b/controller/federationcontroller.php @@ -27,12 +27,11 @@ use OCP\IRequest; use OCP\ISession; use OCP\Share\IManager; -use OCA\Files_Sharing\External\Storage as SharingExternalStorage; - use OCA\Onlyoffice\AppConfig; use OCA\Onlyoffice\DocumentService; use OCA\Onlyoffice\FileUtility; use OCA\Onlyoffice\KeyManager; +use OCA\Onlyoffice\RemoteInstance; /** * OCS handler @@ -136,8 +135,8 @@ class FederationController extends OCSController { $fileId = $file->getId(); - if ($file->getStorage()->instanceOfStorage(SharingExternalStorage::class)) { - $isLock = KeyManager::lockFederatedKey($file, $lock, $fs); + if (RemoteInstance::isRemoteFile($file)) { + $isLock = RemoteInstance::lockRemoteKey($file, $lock, $fs); if (!$isLock) { return new DataResponse(["error" => "Failed request"]); } @@ -151,4 +150,19 @@ class FederationController extends OCSController { $this->logger->debug("Federated request lock for " . $fileId, ["app" => $this->appName]); return new DataResponse(); } + + /** + * Health check instance + * + * @return DataResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + */ + public function healthcheck() { + $this->logger->debug("Federated healthcheck", ["app" => $this->appName]); + + return new DataResponse(["alive" => true]); + } } diff --git a/lib/Migration/Version070400Date20220929111111.php b/lib/Migration/Version070400Date20220929111111.php index 0256498..d42b3db 100644 --- a/lib/Migration/Version070400Date20220929111111.php +++ b/lib/Migration/Version070400Date20220929111111.php @@ -76,6 +76,28 @@ class Version070400Date20220929111111 extends SimpleMigrationStep { $table->addUniqueIndex(['share_id'], 'onlyoffice_share_id_index'); } + if (!$schema->hasTable('onlyoffice_instance')) { + $table = $schema->createTable('onlyoffice_instance'); + $table->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('remote', 'string', [ + 'notnull' => true, + 'length' => 128, + ]); + $table->addColumn('expire', 'bigint', [ + 'notnull' => true, + 'default' => 0, + ]); + $table->addColumn('status', 'integer', [ + 'notnull' => true, + 'default' => 0, + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['remote'], 'onlyoffice_remote_index'); + } + return $schema; } diff --git a/lib/fileutility.php b/lib/fileutility.php index 2d2a65f..cc50a45 100644 --- a/lib/fileutility.php +++ b/lib/fileutility.php @@ -27,10 +27,9 @@ use OCP\ILogger; use OCP\ISession; use OCP\Share\IManager; -use OCA\Files_Sharing\External\Storage as SharingExternalStorage; - use OCA\Onlyoffice\AppConfig; use OCA\Onlyoffice\KeyManager; +use OCA\Onlyoffice\RemoteInstance; /** * File utility @@ -221,10 +220,10 @@ class FileUtility { $fileId = $file->getId(); if ($origin - && $file->getStorage()->instanceOfStorage(SharingExternalStorage::class)) { + && RemoteInstance::isRemoteFile($file)) { try { - $key = $this->getFederatedKey($file); + $key = RemoteInstance::getRemoteKey($file); if (!empty($key)) { return $key; @@ -263,41 +262,6 @@ class FileUtility { } /** - * Generate unique document identifier in federated share - * - * @param File $file - file - * - * @return string - */ - private function getFederatedKey($file) { - $remote = rtrim($file->getStorage()->getRemote(), "/") . "/"; - $shareToken = $file->getStorage()->getToken(); - $internalPath = $file->getInternalPath(); - - $httpClientService = \OC::$server->getHTTPClientService(); - $client = $httpClientService->newClient(); - $response = $client->post($remote . "ocs/v2.php/apps/" . $this->appName . "/api/v1/key?format=json", [ - "timeout" => 5, - "body" => [ - "shareToken" => $shareToken, - "path" => $internalPath - ] - ]); - $body = \json_decode($response->getBody(), true); - - $data = $body["ocs"]["data"]; - if (!empty($data["error"])) { - $this->logger->error("Error federated key " . $data["error"], ["app" => $this->appName]); - return null; - } - - $key = $data["key"]; - $this->logger->debug("Federated key: $key", ["app" => $this->appName]); - - return $key; - } - - /** * Generate unique file version key * * @param OCA\Files_Versions\Versions\IVersion $version - file version diff --git a/lib/keymanager.php b/lib/keymanager.php index f86f949..bdde78c 100644 --- a/lib/keymanager.php +++ b/lib/keymanager.php @@ -19,8 +19,6 @@ namespace OCA\Onlyoffice; -use OCP\Files\File; - /** * Key manager * @@ -29,11 +27,6 @@ use OCP\Files\File; class KeyManager { /** - * App name - */ - private const App_Name = "onlyoffice"; - - /** * Table name */ private const TableName_Key = "onlyoffice_filekey"; @@ -153,56 +146,4 @@ class KeyManager { return $fs === "1"; } - - /** - * Change lock status in the federated share - * - * @param File $file - file - * @param bool $lock - status - * @param bool $fs - status - * - * @return bool - */ - public static function lockFederatedKey($file, $lock, $fs) { - $logger = \OC::$server->getLogger(); - $action = $lock ? "lock" : "unlock"; - - $remote = rtrim($file->getStorage()->getRemote(), "/") . "/"; - $shareToken = $file->getStorage()->getToken(); - $internalPath = $file->getInternalPath(); - - $httpClientService = \OC::$server->getHTTPClientService(); - $client = $httpClientService->newClient(); - $data = [ - "timeout" => 5, - "body" => [ - "shareToken" => $shareToken, - "path" => $internalPath, - "lock" => $lock - ] - ]; - if (!empty($fs)) { - $data["body"]["fs"] = $fs; - } - - try { - $response = $client->post($remote . "ocs/v2.php/apps/" . self::App_Name . "/api/v1/keylock?format=json", $data); - $body = \json_decode($response->getBody(), true); - - $data = $body["ocs"]["data"]; - - if (empty($data)) { - $logger->debug("Federated request " . $action . " for " . $file->getFileInfo()->getId() . " is successful", ["app" => self::App_Name]); - return true; - } - - if (!empty($data["error"])) { - $logger->error("Error " . $action . " federated key for " . $file->getFileInfo()->getId() . ": " . $data["error"], ["app" => self::App_Name]); - return false; - } - } catch (\Exception $e) { - $logger->logException($e, ["message" => "Failed to request federated " . $action . " for " . $file->getFileInfo()->getId(), "app" => self::App_Name]); - return false; - } - } } diff --git a/lib/remoteinstance.php b/lib/remoteinstance.php new file mode 100644 index 0000000..000de44 --- /dev/null +++ b/lib/remoteinstance.php @@ -0,0 +1,272 @@ +<?php +/** + * + * (c) Copyright Ascensio System SIA 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +namespace OCA\Onlyoffice; + +use OCP\Files\File; + +use OCA\Files_Sharing\External\Storage as SharingExternalStorage; + +/** + * Remote instance manager + * + * @package OCA\Onlyoffice + */ +class RemoteInstance { + + /** + * App name + */ + private const App_Name = "onlyoffice"; + + /** + * Table name + */ + private const TableName_Key = "onlyoffice_instance"; + + /** + * Time to live of remote instance (12 hours) + */ + private static $ttl = 60 * 60 * 12; + + /** + * Health remote list + */ + private static $healthRemote = []; + + /** + * Get remote instance + * + * @param string $remote - remote instance + * + * @return array + */ + private static function get($remote) { + $connection = \OC::$server->getDatabaseConnection(); + $select = $connection->prepare(" + SELECT remote, expire, status + FROM `*PREFIX*" . self::TableName_Key . "` + WHERE `remote` = ? + "); + $result = $select->execute([$remote]); + + $dbremote = $result ? $select->fetch() : []; + + return $dbremote; + } + + /** + * Store remote instance + * + * @param string $remote - remote instance + * @param bool $status - remote status + * + * @return bool + */ + private static function set($remote, $status) { + $connection = \OC::$server->getDatabaseConnection(); + $insert = $connection->prepare(" + INSERT INTO `*PREFIX*" . self::TableName_Key . "` + (`remote`, `status`, `expire`) + VALUES (?, ?, ?) + "); + return (bool)$insert->execute([$remote, $status === true ? 1 : 0, time()]); + } + + /** + * Update remote instance + * + * @param string $remote - remote instance + * @param bool $status - remote status + * + * @return bool + */ + private static function update($remote, $status) { + $connection = \OC::$server->getDatabaseConnection(); + $update = $connection->prepare(" + UPDATE `*PREFIX*" . self::TableName_Key . "` + SET status = ?, expire = ? + WHERE remote = ? + "); + return (bool)$update->execute([$status === true ? 1 : 0, time(), $remote]); + } + + /** + * Health check remote instance + * + * @param string $remote - remote instance + * + * @return bool + */ + public static function healthCheck($remote) { + $logger = \OC::$server->getLogger(); + $remote = rtrim($remote, "/") . "/"; + + if (array_key_exists($remote, self::$healthRemote)) { + $logger->debug("Remote instance " . $remote . " from local cache", ["app" => self::App_Name]); + return self::$healthRemote[$remote]; + } + + $dbremote = self::get($remote); + if (!empty($dbremote) && $dbremote["expire"] + self::$ttl > time()) { + $logger->debug("Remote instance " . $remote . " from database status " . $dbremote["status"], ["app" => self::App_Name]); + self::$healthRemote[$remote] = $dbremote["status"]; + return self::$healthRemote[$remote]; + } + + $httpClientService = \OC::$server->getHTTPClientService(); + $client = $httpClientService->newClient(); + + $status = false; + try { + $response = $client->get($remote . "ocs/v2.php/apps/" . self::App_Name . "/api/v1/healthcheck?format=json"); + $body = json_decode($response->getBody(), true); + + $data = $body["ocs"]["data"]; + + if (isset($data["alive"])) { + $status = $data["alive"] === true; + } + + } catch (\Exception $e) { + $logger->logException($e, ["message" => "Failed to request federated health check for" . $remote, "app" => self::App_Name]); + } + + if (empty($dbremote)) { + self::set($remote, $status); + } else { + self::update($remote, $status); + } + + $logger->debug("Remote instance " . $remote . " was stored to database status " . $dbremote["status"], ["app" => self::App_Name]); + + self::$healthRemote[$remote] = $status; + + return self::$healthRemote[$remote]; + } + + /** + * Generate unique document identifier in federated share + * + * @param File $file - file + * + * @return string + */ + public static function getRemoteKey($file) { + $logger = \OC::$server->getLogger(); + + $remote = rtrim($file->getStorage()->getRemote(), "/") . "/"; + $shareToken = $file->getStorage()->getToken(); + $internalPath = $file->getInternalPath(); + + $httpClientService = \OC::$server->getHTTPClientService(); + $client = $httpClientService->newClient(); + $response = $client->post($remote . "ocs/v2.php/apps/" . self::App_Name . "/api/v1/key?format=json", [ + "timeout" => 5, + "body" => [ + "shareToken" => $shareToken, + "path" => $internalPath + ] + ]); + $body = \json_decode($response->getBody(), true); + + $data = $body["ocs"]["data"]; + if (!empty($data["error"])) { + $logger->error("Error federated key " . $data["error"], ["app" => self::App_Name]); + return null; + } + + $key = $data["key"]; + $logger->debug("Federated key: $key", ["app" => self::App_Name]); + + return $key; + } + + /** + * Change lock status in the federated share + * + * @param File $file - file + * @param bool $lock - status + * @param bool $fs - status + * + * @return bool + */ + public static function lockRemoteKey($file, $lock, $fs) { + $logger = \OC::$server->getLogger(); + $action = $lock ? "lock" : "unlock"; + + $remote = rtrim($file->getStorage()->getRemote(), "/") . "/"; + $shareToken = $file->getStorage()->getToken(); + $internalPath = $file->getInternalPath(); + + $httpClientService = \OC::$server->getHTTPClientService(); + $client = $httpClientService->newClient(); + $data = [ + "timeout" => 5, + "body" => [ + "shareToken" => $shareToken, + "path" => $internalPath, + "lock" => $lock + ] + ]; + if (!empty($fs)) { + $data["body"]["fs"] = $fs; + } + + try { + $response = $client->post($remote . "ocs/v2.php/apps/" . self::App_Name . "/api/v1/keylock?format=json", $data); + $body = \json_decode($response->getBody(), true); + + $data = $body["ocs"]["data"]; + + if (empty($data)) { + $logger->debug("Federated request " . $action . " for " . $file->getFileInfo()->getId() . " is successful", ["app" => self::App_Name]); + return true; + } + + if (!empty($data["error"])) { + $logger->error("Error " . $action . " federated key for " . $file->getFileInfo()->getId() . ": " . $data["error"], ["app" => self::App_Name]); + return false; + } + } catch (\Exception $e) { + $logger->logException($e, ["message" => "Failed to request federated " . $action . " for " . $file->getFileInfo()->getId(), "app" => self::App_Name]); + return false; + } + } + + /** + * Check of federated capable + * + * @param File $file - file + * + * @return bool + */ + public static function isRemoteFile($file) { + $storage = $file->getStorage(); + + $alive = false; + $isFederated = $storage->instanceOfStorage(SharingExternalStorage::class); + if (!$isFederated) { + return false; + } + + $alive = RemoteInstance::healthCheck($storage->getRemote()); + return $alive; + } +}
\ No newline at end of file |