LegacyVersionsBackend.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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 Exception;
  9. use OC\Files\View;
  10. use OCA\DAV\Connector\Sabre\Exception\Forbidden;
  11. use OCA\Files_Sharing\ISharedStorage;
  12. use OCA\Files_Sharing\SharedStorage;
  13. use OCA\Files_Versions\Db\VersionEntity;
  14. use OCA\Files_Versions\Db\VersionsMapper;
  15. use OCA\Files_Versions\Storage;
  16. use OCP\Files\File;
  17. use OCP\Files\FileInfo;
  18. use OCP\Files\Folder;
  19. use OCP\Files\IMimeTypeLoader;
  20. use OCP\Files\IRootFolder;
  21. use OCP\Files\Node;
  22. use OCP\Files\NotFoundException;
  23. use OCP\Files\Storage\IStorage;
  24. use OCP\IUser;
  25. use OCP\IUserManager;
  26. use OCP\IUserSession;
  27. use Psr\Log\LoggerInterface;
  28. class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend, IMetadataVersionBackend, IVersionsImporterBackend {
  29. public function __construct(
  30. private IRootFolder $rootFolder,
  31. private IUserManager $userManager,
  32. private VersionsMapper $versionsMapper,
  33. private IMimeTypeLoader $mimeTypeLoader,
  34. private IUserSession $userSession,
  35. private LoggerInterface $logger,
  36. ) {
  37. }
  38. public function useBackendForStorage(IStorage $storage): bool {
  39. return true;
  40. }
  41. public function getVersionsForFile(IUser $user, FileInfo $file): array {
  42. $storage = $file->getStorage();
  43. if ($storage->instanceOfStorage(SharedStorage::class)) {
  44. $owner = $storage->getOwner('');
  45. $user = $this->userManager->get($owner);
  46. $fileId = $file->getId();
  47. if ($fileId === null) {
  48. throw new NotFoundException("File not found ($fileId)");
  49. }
  50. if ($user === null) {
  51. throw new NotFoundException("User $owner not found for $fileId");
  52. }
  53. $userFolder = $this->rootFolder->getUserFolder($user->getUID());
  54. $file = $userFolder->getFirstNodeById($fileId);
  55. if (!$file) {
  56. throw new NotFoundException("version file not found for share owner");
  57. }
  58. } else {
  59. $userFolder = $this->rootFolder->getUserFolder($user->getUID());
  60. }
  61. $fileId = $file->getId();
  62. if ($fileId === null) {
  63. throw new NotFoundException("File not found ($fileId)");
  64. }
  65. // Insert entries in the DB for existing versions.
  66. $relativePath = $userFolder->getRelativePath($file->getPath());
  67. if ($relativePath === null) {
  68. throw new NotFoundException("Relative path not found for file $fileId (" . $file->getPath() . ')');
  69. }
  70. $currentVersion = [
  71. 'version' => (string)$file->getMtime(),
  72. 'size' => $file->getSize(),
  73. 'mimetype' => $file->getMimetype(),
  74. ];
  75. $versionsInDB = $this->versionsMapper->findAllVersionsForFileId($file->getId());
  76. /** @var array<int, array> */
  77. $versionsInFS = array_values(Storage::getVersions($user->getUID(), $relativePath));
  78. /** @var array<int, array{db: ?VersionEntity, fs: ?mixed}> */
  79. $groupedVersions = [];
  80. $davVersions = [];
  81. foreach ($versionsInDB as $version) {
  82. $revisionId = $version->getTimestamp();
  83. $groupedVersions[$revisionId] = $groupedVersions[$revisionId] ?? [];
  84. $groupedVersions[$revisionId]['db'] = $version;
  85. }
  86. foreach ([$currentVersion, ...$versionsInFS] as $version) {
  87. $revisionId = $version['version'];
  88. $groupedVersions[$revisionId] = $groupedVersions[$revisionId] ?? [];
  89. $groupedVersions[$revisionId]['fs'] = $version;
  90. }
  91. /** @var array<string, array{db: ?VersionEntity, fs: ?mixed}> $groupedVersions */
  92. foreach ($groupedVersions as $versions) {
  93. if (empty($versions['db']) && !empty($versions['fs'])) {
  94. $versions['db'] = new VersionEntity();
  95. $versions['db']->setFileId($fileId);
  96. $versions['db']->setTimestamp((int)$versions['fs']['version']);
  97. $versions['db']->setSize((int)$versions['fs']['size']);
  98. $versions['db']->setMimetype($this->mimeTypeLoader->getId($versions['fs']['mimetype']));
  99. $versions['db']->setMetadata([]);
  100. $this->versionsMapper->insert($versions['db']);
  101. } elseif (!empty($versions['db']) && empty($versions['fs'])) {
  102. $this->versionsMapper->delete($versions['db']);
  103. continue;
  104. }
  105. $version = new Version(
  106. $versions['db']->getTimestamp(),
  107. $versions['db']->getTimestamp(),
  108. $file->getName(),
  109. $versions['db']->getSize(),
  110. $this->mimeTypeLoader->getMimetypeById($versions['db']->getMimetype()),
  111. $userFolder->getRelativePath($file->getPath()),
  112. $file,
  113. $this,
  114. $user,
  115. $versions['db']->getMetadata() ?? [],
  116. );
  117. array_push($davVersions, $version);
  118. }
  119. return $davVersions;
  120. }
  121. public function createVersion(IUser $user, FileInfo $file) {
  122. $userFolder = $this->rootFolder->getUserFolder($user->getUID());
  123. $relativePath = $userFolder->getRelativePath($file->getPath());
  124. $userView = new View('/' . $user->getUID());
  125. // create all parent folders
  126. Storage::createMissingDirectories($relativePath, $userView);
  127. Storage::scheduleExpire($user->getUID(), $relativePath);
  128. // store a new version of a file
  129. $userView->copy('files/' . $relativePath, 'files_versions/' . $relativePath . '.v' . $file->getMtime());
  130. // ensure the file is scanned
  131. $userView->getFileInfo('files_versions/' . $relativePath . '.v' . $file->getMtime());
  132. }
  133. public function rollback(IVersion $version) {
  134. if (!$this->currentUserHasPermissions($version->getSourceFile(), \OCP\Constants::PERMISSION_UPDATE)) {
  135. throw new Forbidden('You cannot restore this version because you do not have update permissions on the source file.');
  136. }
  137. return Storage::rollback($version->getVersionPath(), $version->getRevisionId(), $version->getUser());
  138. }
  139. private function getVersionFolder(IUser $user): Folder {
  140. $userRoot = $this->rootFolder->getUserFolder($user->getUID())
  141. ->getParent();
  142. try {
  143. /** @var Folder $folder */
  144. $folder = $userRoot->get('files_versions');
  145. return $folder;
  146. } catch (NotFoundException $e) {
  147. return $userRoot->newFolder('files_versions');
  148. }
  149. }
  150. public function read(IVersion $version) {
  151. $versions = $this->getVersionFolder($version->getUser());
  152. /** @var File $file */
  153. $file = $versions->get($version->getVersionPath() . '.v' . $version->getRevisionId());
  154. return $file->fopen('r');
  155. }
  156. public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File {
  157. $userFolder = $this->rootFolder->getUserFolder($user->getUID());
  158. $owner = $sourceFile->getOwner();
  159. $storage = $sourceFile->getStorage();
  160. // Shared files have their versions in the owners root folder so we need to obtain them from there
  161. if ($storage->instanceOfStorage(ISharedStorage::class) && $owner) {
  162. /** @var SharedStorage $storage */
  163. $userFolder = $this->rootFolder->getUserFolder($owner->getUID());
  164. $user = $owner;
  165. $ownerPathInStorage = $sourceFile->getInternalPath();
  166. $sourceFile = $storage->getShare()->getNode();
  167. if ($sourceFile instanceof Folder) {
  168. $sourceFile = $sourceFile->get($ownerPathInStorage);
  169. }
  170. }
  171. $versionFolder = $this->getVersionFolder($user);
  172. /** @var File $file */
  173. $file = $versionFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . '.v' . $revision);
  174. return $file;
  175. }
  176. public function deleteVersion(IVersion $version): void {
  177. if (!$this->currentUserHasPermissions($version->getSourceFile(), \OCP\Constants::PERMISSION_DELETE)) {
  178. throw new Forbidden('You cannot delete this version because you do not have delete permissions on the source file.');
  179. }
  180. Storage::deleteRevision($version->getVersionPath(), $version->getRevisionId());
  181. $versionEntity = $this->versionsMapper->findVersionForFileId(
  182. $version->getSourceFile()->getId(),
  183. $version->getTimestamp(),
  184. );
  185. $this->versionsMapper->delete($versionEntity);
  186. }
  187. public function createVersionEntity(File $file): void {
  188. $versionEntity = new VersionEntity();
  189. $versionEntity->setFileId($file->getId());
  190. $versionEntity->setTimestamp($file->getMTime());
  191. $versionEntity->setSize($file->getSize());
  192. $versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype()));
  193. $versionEntity->setMetadata([]);
  194. $this->versionsMapper->insert($versionEntity);
  195. }
  196. public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void {
  197. $versionEntity = $this->versionsMapper->findVersionForFileId($sourceFile->getId(), $revision);
  198. if (isset($properties['timestamp'])) {
  199. $versionEntity->setTimestamp($properties['timestamp']);
  200. }
  201. if (isset($properties['size'])) {
  202. $versionEntity->setSize($properties['size']);
  203. }
  204. if (isset($properties['mimetype'])) {
  205. $versionEntity->setMimetype($properties['mimetype']);
  206. }
  207. $this->versionsMapper->update($versionEntity);
  208. }
  209. public function deleteVersionsEntity(File $file): void {
  210. $this->versionsMapper->deleteAllVersionsForFileId($file->getId());
  211. }
  212. private function currentUserHasPermissions(FileInfo $sourceFile, int $permissions): bool {
  213. $currentUserId = $this->userSession->getUser()?->getUID();
  214. if ($currentUserId === null) {
  215. throw new NotFoundException("No user logged in");
  216. }
  217. if ($sourceFile->getOwner()?->getUID() === $currentUserId) {
  218. return ($sourceFile->getPermissions() & $permissions) === $permissions;
  219. }
  220. $nodes = $this->rootFolder->getUserFolder($currentUserId)->getById($sourceFile->getId());
  221. if (count($nodes) === 0) {
  222. throw new NotFoundException("Version file not accessible by current user");
  223. }
  224. foreach ($nodes as $node) {
  225. if (($node->getPermissions() & $permissions) === $permissions) {
  226. return true;
  227. }
  228. }
  229. return false;
  230. }
  231. public function setMetadataValue(Node $node, int $revision, string $key, string $value): void {
  232. if (!$this->currentUserHasPermissions($node, \OCP\Constants::PERMISSION_UPDATE)) {
  233. throw new Forbidden('You cannot update the version\'s metadata because you do not have update permissions on the source file.');
  234. }
  235. $versionEntity = $this->versionsMapper->findVersionForFileId($node->getId(), $revision);
  236. $versionEntity->setMetadataValue($key, $value);
  237. $this->versionsMapper->update($versionEntity);
  238. }
  239. /**
  240. * @inheritdoc
  241. */
  242. public function importVersionsForFile(IUser $user, Node $source, Node $target, array $versions): void {
  243. $userFolder = $this->rootFolder->getUserFolder($user->getUID());
  244. $relativePath = $userFolder->getRelativePath($target->getPath());
  245. if ($relativePath === null) {
  246. throw new \Exception('Target does not have a relative path' . $target->getPath());
  247. }
  248. $userView = new View('/' . $user->getUID());
  249. // create all parent folders
  250. Storage::createMissingDirectories($relativePath, $userView);
  251. Storage::scheduleExpire($user->getUID(), $relativePath);
  252. foreach ($versions as $version) {
  253. // 1. Import the file in its new location.
  254. // Nothing to do for the current version.
  255. if ($version->getTimestamp() !== $source->getMTime()) {
  256. $backend = $version->getBackend();
  257. $versionFile = $backend->getVersionFile($user, $source, $version->getRevisionId());
  258. $newVersionPath = 'files_versions/' . $relativePath . '.v' . $version->getTimestamp();
  259. $versionContent = $versionFile->fopen('r');
  260. if ($versionContent === false) {
  261. $this->logger->warning('Fail to open version file.', ['source' => $source, 'version' => $version, 'versionFile' => $versionFile]);
  262. continue;
  263. }
  264. $userView->file_put_contents($newVersionPath, $versionContent);
  265. // ensure the file is scanned
  266. $userView->getFileInfo($newVersionPath);
  267. }
  268. // 2. Create the entity in the database
  269. $versionEntity = new VersionEntity();
  270. $versionEntity->setFileId($target->getId());
  271. $versionEntity->setTimestamp($version->getTimestamp());
  272. $versionEntity->setSize($version->getSize());
  273. $versionEntity->setMimetype($this->mimeTypeLoader->getId($version->getMimetype()));
  274. if ($version instanceof IMetadataVersion) {
  275. $versionEntity->setMetadata($version->getMetadata());
  276. }
  277. $this->versionsMapper->insert($versionEntity);
  278. }
  279. }
  280. /**
  281. * @inheritdoc
  282. */
  283. public function clearVersionsForFile(IUser $user, Node $source, Node $target): void {
  284. $userFolder = $this->rootFolder->getUserFolder($user->getUID());
  285. $relativePath = $userFolder->getRelativePath($source->getPath());
  286. if ($relativePath === null) {
  287. throw new Exception("Relative path not found for node with path: " . $source->getPath());
  288. }
  289. $versions = Storage::getVersions($user->getUID(), $relativePath);
  290. /** @var Folder versionFolder */
  291. $versionFolder = $this->rootFolder->get('admin/files_versions');
  292. foreach ($versions as $version) {
  293. $versionFolder->get($version['path'] . '.v' . (int)$version['version'])->delete();
  294. }
  295. $this->versionsMapper->deleteAllVersionsForFileId($target->getId());
  296. }
  297. }