VersionManager.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\Files_Versions\Versions;
  8. use OCP\Files\File;
  9. use OCP\Files\FileInfo;
  10. use OCP\Files\IRootFolder;
  11. use OCP\Files\Lock\ILock;
  12. use OCP\Files\Lock\ILockManager;
  13. use OCP\Files\Lock\LockContext;
  14. use OCP\Files\Node;
  15. use OCP\Files\Storage\IStorage;
  16. use OCP\IUser;
  17. use OCP\Lock\ManuallyLockedException;
  18. class VersionManager implements IVersionManager, IDeletableVersionBackend, INeedSyncVersionBackend, IMetadataVersionBackend {
  19. /** @var (IVersionBackend[])[] */
  20. private $backends = [];
  21. public function registerBackend(string $storageType, IVersionBackend $backend) {
  22. if (!isset($this->backends[$storageType])) {
  23. $this->backends[$storageType] = [];
  24. }
  25. $this->backends[$storageType][] = $backend;
  26. }
  27. /**
  28. * @return (IVersionBackend[])[]
  29. */
  30. private function getBackends(): array {
  31. return $this->backends;
  32. }
  33. /**
  34. * @param IStorage $storage
  35. * @return IVersionBackend
  36. * @throws BackendNotFoundException
  37. */
  38. public function getBackendForStorage(IStorage $storage): IVersionBackend {
  39. $fullType = get_class($storage);
  40. $backends = $this->getBackends();
  41. $foundType = '';
  42. $foundBackend = null;
  43. foreach ($backends as $type => $backendsForType) {
  44. if (
  45. $storage->instanceOfStorage($type) &&
  46. ($foundType === '' || is_subclass_of($type, $foundType))
  47. ) {
  48. foreach ($backendsForType as $backend) {
  49. /** @var IVersionBackend $backend */
  50. if ($backend->useBackendForStorage($storage)) {
  51. $foundBackend = $backend;
  52. $foundType = $type;
  53. }
  54. }
  55. }
  56. }
  57. if ($foundType === '' || $foundBackend === null) {
  58. throw new BackendNotFoundException("Version backend for $fullType not found");
  59. } else {
  60. return $foundBackend;
  61. }
  62. }
  63. public function getVersionsForFile(IUser $user, FileInfo $file): array {
  64. $backend = $this->getBackendForStorage($file->getStorage());
  65. return $backend->getVersionsForFile($user, $file);
  66. }
  67. public function createVersion(IUser $user, FileInfo $file) {
  68. $backend = $this->getBackendForStorage($file->getStorage());
  69. $backend->createVersion($user, $file);
  70. }
  71. public function rollback(IVersion $version) {
  72. $backend = $version->getBackend();
  73. $result = self::handleAppLocks(fn (): ?bool => $backend->rollback($version));
  74. // rollback doesn't have a return type yet and some implementations don't return anything
  75. if ($result === null || $result === true) {
  76. \OC_Hook::emit('\OCP\Versions', 'rollback', [
  77. 'path' => $version->getVersionPath(),
  78. 'revision' => $version->getRevisionId(),
  79. 'node' => $version->getSourceFile(),
  80. ]);
  81. }
  82. return $result;
  83. }
  84. public function read(IVersion $version) {
  85. $backend = $version->getBackend();
  86. return $backend->read($version);
  87. }
  88. public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File {
  89. $backend = $this->getBackendForStorage($sourceFile->getStorage());
  90. return $backend->getVersionFile($user, $sourceFile, $revision);
  91. }
  92. public function useBackendForStorage(IStorage $storage): bool {
  93. return false;
  94. }
  95. public function deleteVersion(IVersion $version): void {
  96. $backend = $version->getBackend();
  97. if ($backend instanceof IDeletableVersionBackend) {
  98. $backend->deleteVersion($version);
  99. }
  100. }
  101. public function createVersionEntity(File $file): void {
  102. $backend = $this->getBackendForStorage($file->getStorage());
  103. if ($backend instanceof INeedSyncVersionBackend) {
  104. $backend->createVersionEntity($file);
  105. }
  106. }
  107. public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void {
  108. $backend = $this->getBackendForStorage($sourceFile->getStorage());
  109. if ($backend instanceof INeedSyncVersionBackend) {
  110. $backend->updateVersionEntity($sourceFile, $revision, $properties);
  111. }
  112. }
  113. public function deleteVersionsEntity(File $file): void {
  114. $backend = $this->getBackendForStorage($file->getStorage());
  115. if ($backend instanceof INeedSyncVersionBackend) {
  116. $backend->deleteVersionsEntity($file);
  117. }
  118. }
  119. public function setMetadataValue(Node $node, int $revision, string $key, string $value): void {
  120. $backend = $this->getBackendForStorage($node->getStorage());
  121. if ($backend instanceof IMetadataVersionBackend) {
  122. $backend->setMetadataValue($node, $revision, $key, $value);
  123. }
  124. }
  125. /**
  126. * Catch ManuallyLockedException and retry in app context if possible.
  127. *
  128. * Allow users to go back to old versions via the versions tab in the sidebar
  129. * even when the file is opened in the viewer next to it.
  130. *
  131. * Context: If a file is currently opened for editing
  132. * the files_lock app will throw ManuallyLockedExceptions.
  133. * This prevented the user from rolling an opened file back to a previous version.
  134. *
  135. * Text and Richdocuments can handle changes of open files.
  136. * So we execute the rollback under their lock context
  137. * to let them handle the conflict.
  138. *
  139. * @param callable $callback function to run with app locks handled
  140. * @return bool|null
  141. * @throws ManuallyLockedException
  142. *
  143. */
  144. private static function handleAppLocks(callable $callback): ?bool {
  145. try {
  146. return $callback();
  147. } catch (ManuallyLockedException $e) {
  148. $owner = (string)$e->getOwner();
  149. $appsThatHandleUpdates = ['text', 'richdocuments'];
  150. if (!in_array($owner, $appsThatHandleUpdates)) {
  151. throw $e;
  152. }
  153. // The LockWrapper in the files_lock app only compares the lock type and owner
  154. // when checking the lock against the current scope.
  155. // So we do not need to get the actual node here
  156. // and use the root node instead.
  157. $root = \OC::$server->get(IRootFolder::class);
  158. $lockContext = new LockContext($root, ILock::TYPE_APP, $owner);
  159. $lockManager = \OC::$server->get(ILockManager::class);
  160. $result = null;
  161. $lockManager->runInScope($lockContext, function () use ($callback, &$result): void {
  162. $result = $callback();
  163. });
  164. return $result;
  165. }
  166. }
  167. }