LegacyVersionsBackend.php 13 KB

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