1
0

LegacyVersionsBackend.php 10 KB

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