LegacyVersionsBackend.php 11 KB

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