SharedMount.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\Files_Sharing;
  8. use OC\Files\Filesystem;
  9. use OC\Files\Mount\MountPoint;
  10. use OC\Files\Mount\MoveableMount;
  11. use OC\Files\View;
  12. use OCA\Files_Sharing\Exceptions\BrokenPath;
  13. use OCP\Cache\CappedMemoryCache;
  14. use OCP\EventDispatcher\IEventDispatcher;
  15. use OCP\Files\Events\InvalidateMountCacheEvent;
  16. use OCP\Files\Storage\IStorageFactory;
  17. use OCP\ICache;
  18. use OCP\IUser;
  19. use OCP\Server;
  20. use OCP\Share\Events\VerifyMountPointEvent;
  21. use OCP\Share\IShare;
  22. use Psr\Log\LoggerInterface;
  23. /**
  24. * Shared mount points can be moved by the user
  25. */
  26. class SharedMount extends MountPoint implements MoveableMount, ISharedMountPoint {
  27. /**
  28. * @var SharedStorage $storage
  29. */
  30. protected $storage = null;
  31. /** @var IShare */
  32. private $superShare;
  33. /** @var IShare[] */
  34. private $groupedShares;
  35. public function __construct(
  36. $storage,
  37. array $mountpoints,
  38. $arguments,
  39. IStorageFactory $loader,
  40. private View $recipientView,
  41. CappedMemoryCache $folderExistCache,
  42. private IEventDispatcher $eventDispatcher,
  43. private IUser $user,
  44. private ICache $cache,
  45. ) {
  46. $this->superShare = $arguments['superShare'];
  47. $this->groupedShares = $arguments['groupedShares'];
  48. $newMountPoint = $this->verifyMountPoint($this->superShare, $mountpoints, $folderExistCache);
  49. $absMountPoint = '/' . $user->getUID() . '/files' . $newMountPoint;
  50. parent::__construct($storage, $absMountPoint, $arguments, $loader, null, null, MountProvider::class);
  51. }
  52. /**
  53. * check if the parent folder exists otherwise move the mount point up
  54. *
  55. * @param IShare $share
  56. * @param SharedMount[] $mountpoints
  57. * @param CappedMemoryCache<bool> $folderExistCache
  58. * @return string
  59. */
  60. private function verifyMountPoint(
  61. IShare $share,
  62. array $mountpoints,
  63. CappedMemoryCache $folderExistCache,
  64. ) {
  65. $cacheKey = $this->user->getUID() . '/' . $share->getId() . '/' . $share->getTarget();
  66. $cached = $this->cache->get($cacheKey);
  67. if ($cached !== null) {
  68. return $cached;
  69. }
  70. $mountPoint = basename($share->getTarget());
  71. $parent = dirname($share->getTarget());
  72. $event = new VerifyMountPointEvent($share, $this->recipientView, $parent);
  73. $this->eventDispatcher->dispatchTyped($event);
  74. $parent = $event->getParent();
  75. $cached = $folderExistCache->get($parent);
  76. if ($cached) {
  77. $parentExists = $cached;
  78. } else {
  79. $parentExists = $this->recipientView->is_dir($parent);
  80. $folderExistCache->set($parent, $parentExists);
  81. }
  82. if (!$parentExists) {
  83. $parent = Helper::getShareFolder($this->recipientView, $this->user->getUID());
  84. }
  85. $newMountPoint = $this->generateUniqueTarget(
  86. Filesystem::normalizePath($parent . '/' . $mountPoint),
  87. $this->recipientView,
  88. $mountpoints
  89. );
  90. if ($newMountPoint !== $share->getTarget()) {
  91. $this->updateFileTarget($newMountPoint, $share);
  92. }
  93. $this->cache->set($cacheKey, $newMountPoint, 60 * 60);
  94. return $newMountPoint;
  95. }
  96. /**
  97. * update fileTarget in the database if the mount point changed
  98. *
  99. * @param string $newPath
  100. * @param IShare $share
  101. * @return bool
  102. */
  103. private function updateFileTarget($newPath, &$share) {
  104. $share->setTarget($newPath);
  105. foreach ($this->groupedShares as $tmpShare) {
  106. $tmpShare->setTarget($newPath);
  107. \OC::$server->getShareManager()->moveShare($tmpShare, $this->user->getUID());
  108. }
  109. $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($this->user));
  110. }
  111. /**
  112. * @param string $path
  113. * @param View $view
  114. * @param SharedMount[] $mountpoints
  115. * @return mixed
  116. */
  117. private function generateUniqueTarget($path, $view, array $mountpoints) {
  118. $pathinfo = pathinfo($path);
  119. $ext = isset($pathinfo['extension']) ? '.' . $pathinfo['extension'] : '';
  120. $name = $pathinfo['filename'];
  121. $dir = $pathinfo['dirname'];
  122. $i = 2;
  123. $absolutePath = $this->recipientView->getAbsolutePath($path) . '/';
  124. while ($view->file_exists($path) || isset($mountpoints[$absolutePath])) {
  125. $path = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext);
  126. $absolutePath = $this->recipientView->getAbsolutePath($path) . '/';
  127. $i++;
  128. }
  129. return $path;
  130. }
  131. /**
  132. * Format a path to be relative to the /user/files/ directory
  133. *
  134. * @param string $path the absolute path
  135. * @return string e.g. turns '/admin/files/test.txt' into '/test.txt'
  136. * @throws BrokenPath
  137. */
  138. protected function stripUserFilesPath($path) {
  139. $trimmed = ltrim($path, '/');
  140. $split = explode('/', $trimmed);
  141. // it is not a file relative to data/user/files
  142. if (count($split) < 3 || $split[1] !== 'files') {
  143. Server::get(LoggerInterface::class)->error('Can not strip userid and "files/" from path: ' . $path, ['app' => 'files_sharing']);
  144. throw new BrokenPath('Path does not start with /user/files', 10);
  145. }
  146. // skip 'user' and 'files'
  147. $sliced = array_slice($split, 2);
  148. $relPath = implode('/', $sliced);
  149. return '/' . $relPath;
  150. }
  151. /**
  152. * Move the mount point to $target
  153. *
  154. * @param string $target the target mount point
  155. * @return bool
  156. */
  157. public function moveMount($target) {
  158. $relTargetPath = $this->stripUserFilesPath($target);
  159. $share = $this->storage->getShare();
  160. $result = true;
  161. try {
  162. $this->updateFileTarget($relTargetPath, $share);
  163. $this->setMountPoint($target);
  164. $this->storage->setMountPoint($relTargetPath);
  165. } catch (\Exception $e) {
  166. Server::get(LoggerInterface::class)->error(
  167. 'Could not rename mount point for shared folder "' . $this->getMountPoint() . '" to "' . $target . '"',
  168. [
  169. 'app' => 'files_sharing',
  170. 'exception' => $e,
  171. ]
  172. );
  173. }
  174. return $result;
  175. }
  176. /**
  177. * Remove the mount points
  178. *
  179. * @return bool
  180. */
  181. public function removeMount() {
  182. $mountManager = Filesystem::getMountManager();
  183. /** @var SharedStorage $storage */
  184. $storage = $this->getStorage();
  185. $result = $storage->unshareStorage();
  186. $mountManager->removeMount($this->mountPoint);
  187. return $result;
  188. }
  189. /**
  190. * @return IShare
  191. */
  192. public function getShare() {
  193. return $this->superShare;
  194. }
  195. /**
  196. * @return IShare[]
  197. */
  198. public function getGroupedShares(): array {
  199. return $this->groupedShares;
  200. }
  201. /**
  202. * Get the file id of the root of the storage
  203. *
  204. * @return int
  205. */
  206. public function getStorageRootId() {
  207. return $this->getShare()->getNodeId();
  208. }
  209. /**
  210. * @return int
  211. */
  212. public function getNumericStorageId() {
  213. if (!is_null($this->getShare()->getNodeCacheEntry())) {
  214. return $this->getShare()->getNodeCacheEntry()->getStorageId();
  215. } else {
  216. $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
  217. $query = $builder->select('storage')
  218. ->from('filecache')
  219. ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($this->getStorageRootId())));
  220. $result = $query->executeQuery();
  221. $row = $result->fetch();
  222. $result->closeCursor();
  223. if ($row) {
  224. return (int)$row['storage'];
  225. }
  226. return -1;
  227. }
  228. }
  229. public function getMountType() {
  230. return 'shared';
  231. }
  232. }