LegacyVersionsBackend.php 11 KB

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