SMB.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  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_External\Lib\Storage;
  8. use Icewind\SMB\ACL;
  9. use Icewind\SMB\BasicAuth;
  10. use Icewind\SMB\Exception\AlreadyExistsException;
  11. use Icewind\SMB\Exception\ConnectException;
  12. use Icewind\SMB\Exception\Exception;
  13. use Icewind\SMB\Exception\ForbiddenException;
  14. use Icewind\SMB\Exception\InvalidArgumentException;
  15. use Icewind\SMB\Exception\InvalidTypeException;
  16. use Icewind\SMB\Exception\NotFoundException;
  17. use Icewind\SMB\Exception\OutOfSpaceException;
  18. use Icewind\SMB\Exception\TimedOutException;
  19. use Icewind\SMB\IFileInfo;
  20. use Icewind\SMB\Native\NativeServer;
  21. use Icewind\SMB\Options;
  22. use Icewind\SMB\ServerFactory;
  23. use Icewind\SMB\Wrapped\Server;
  24. use Icewind\Streams\CallbackWrapper;
  25. use Icewind\Streams\IteratorDirectory;
  26. use OC\Files\Filesystem;
  27. use OC\Files\Storage\Common;
  28. use OCA\Files_External\Lib\Notify\SMBNotifyHandler;
  29. use OCP\Cache\CappedMemoryCache;
  30. use OCP\Constants;
  31. use OCP\Files\EntityTooLargeException;
  32. use OCP\Files\Notify\IChange;
  33. use OCP\Files\Notify\IRenameChange;
  34. use OCP\Files\NotPermittedException;
  35. use OCP\Files\Storage\INotifyStorage;
  36. use OCP\Files\StorageAuthException;
  37. use OCP\Files\StorageNotAvailableException;
  38. use Psr\Log\LoggerInterface;
  39. class SMB extends Common implements INotifyStorage {
  40. /**
  41. * @var \Icewind\SMB\IServer
  42. */
  43. protected $server;
  44. /**
  45. * @var \Icewind\SMB\IShare
  46. */
  47. protected $share;
  48. /**
  49. * @var string
  50. */
  51. protected $root;
  52. /** @var CappedMemoryCache<IFileInfo> */
  53. protected CappedMemoryCache $statCache;
  54. /** @var LoggerInterface */
  55. protected $logger;
  56. /** @var bool */
  57. protected $showHidden;
  58. private bool $caseSensitive;
  59. /** @var bool */
  60. protected $checkAcl;
  61. public function __construct(array $parameters) {
  62. if (!isset($parameters['host'])) {
  63. throw new \Exception('Invalid configuration, no host provided');
  64. }
  65. if (isset($parameters['auth'])) {
  66. $auth = $parameters['auth'];
  67. } elseif (isset($parameters['user']) && isset($parameters['password']) && isset($parameters['share'])) {
  68. [$workgroup, $user] = $this->splitUser($parameters['user']);
  69. $auth = new BasicAuth($user, $workgroup, $parameters['password']);
  70. } else {
  71. throw new \Exception('Invalid configuration, no credentials provided');
  72. }
  73. if (isset($parameters['logger'])) {
  74. if (!$parameters['logger'] instanceof LoggerInterface) {
  75. throw new \Exception(
  76. 'Invalid logger. Got '
  77. . get_class($parameters['logger'])
  78. . ' Expected ' . LoggerInterface::class
  79. );
  80. }
  81. $this->logger = $parameters['logger'];
  82. } else {
  83. $this->logger = \OCP\Server::get(LoggerInterface::class);
  84. }
  85. $options = new Options();
  86. if (isset($parameters['timeout'])) {
  87. $timeout = (int)$parameters['timeout'];
  88. if ($timeout > 0) {
  89. $options->setTimeout($timeout);
  90. }
  91. }
  92. $system = \OCP\Server::get(SystemBridge::class);
  93. $serverFactory = new ServerFactory($options, $system);
  94. $this->server = $serverFactory->createServer($parameters['host'], $auth);
  95. $this->share = $this->server->getShare(trim($parameters['share'], '/'));
  96. $this->root = $parameters['root'] ?? '/';
  97. $this->root = '/' . ltrim($this->root, '/');
  98. $this->root = rtrim($this->root, '/') . '/';
  99. $this->showHidden = isset($parameters['show_hidden']) && $parameters['show_hidden'];
  100. $this->caseSensitive = (bool)($parameters['case_sensitive'] ?? true);
  101. $this->checkAcl = isset($parameters['check_acl']) && $parameters['check_acl'];
  102. $this->statCache = new CappedMemoryCache();
  103. parent::__construct($parameters);
  104. }
  105. private function splitUser(string $user): array {
  106. if (str_contains($user, '/')) {
  107. return explode('/', $user, 2);
  108. } elseif (str_contains($user, '\\')) {
  109. return explode('\\', $user);
  110. }
  111. return [null, $user];
  112. }
  113. public function getId(): string {
  114. // FIXME: double slash to keep compatible with the old storage ids,
  115. // failure to do so will lead to creation of a new storage id and
  116. // loss of shares from the storage
  117. return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
  118. }
  119. protected function buildPath(string $path): string {
  120. return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
  121. }
  122. protected function relativePath(string $fullPath): ?string {
  123. if ($fullPath === $this->root) {
  124. return '';
  125. } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
  126. return substr($fullPath, strlen($this->root));
  127. } else {
  128. return null;
  129. }
  130. }
  131. /**
  132. * @throws StorageAuthException
  133. * @throws \OCP\Files\NotFoundException
  134. * @throws \OCP\Files\ForbiddenException
  135. */
  136. protected function getFileInfo(string $path): IFileInfo {
  137. try {
  138. $path = $this->buildPath($path);
  139. $cached = $this->statCache[$path] ?? null;
  140. if ($cached instanceof IFileInfo) {
  141. return $cached;
  142. } else {
  143. $stat = $this->share->stat($path);
  144. $this->statCache[$path] = $stat;
  145. return $stat;
  146. }
  147. } catch (ConnectException $e) {
  148. $this->throwUnavailable($e);
  149. } catch (NotFoundException $e) {
  150. throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
  151. } catch (ForbiddenException $e) {
  152. // with php-smbclient, this exception is thrown when the provided password is invalid.
  153. // Possible is also ForbiddenException with a different error code, so we check it.
  154. if ($e->getCode() === 1) {
  155. $this->throwUnavailable($e);
  156. }
  157. throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e);
  158. }
  159. }
  160. /**
  161. * @throws StorageAuthException
  162. */
  163. protected function throwUnavailable(\Exception $e): never {
  164. $this->logger->error('Error while getting file info', ['exception' => $e]);
  165. throw new StorageAuthException($e->getMessage(), $e);
  166. }
  167. /**
  168. * get the acl from fileinfo that is relevant for the configured user
  169. */
  170. private function getACL(IFileInfo $file): ?ACL {
  171. try {
  172. $acls = $file->getAcls();
  173. } catch (Exception $e) {
  174. $this->logger->error('Error while getting file acls', ['exception' => $e]);
  175. return null;
  176. }
  177. foreach ($acls as $user => $acl) {
  178. [, $user] = $this->splitUser($user); // strip domain
  179. if ($user === $this->server->getAuth()->getUsername()) {
  180. return $acl;
  181. }
  182. }
  183. return null;
  184. }
  185. /**
  186. * @return \Generator<IFileInfo>
  187. * @throws StorageNotAvailableException
  188. */
  189. protected function getFolderContents(string $path): iterable {
  190. try {
  191. $path = ltrim($this->buildPath($path), '/');
  192. try {
  193. $files = $this->share->dir($path);
  194. } catch (ForbiddenException $e) {
  195. $this->logger->critical($e->getMessage(), ['exception' => $e]);
  196. throw new NotPermittedException();
  197. } catch (InvalidTypeException $e) {
  198. return;
  199. }
  200. foreach ($files as $file) {
  201. $this->statCache[$path . '/' . $file->getName()] = $file;
  202. }
  203. foreach ($files as $file) {
  204. try {
  205. // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
  206. // so we trigger the below exceptions where applicable
  207. $hide = $file->isHidden() && !$this->showHidden;
  208. if ($this->checkAcl && $acl = $this->getACL($file)) {
  209. // if there is no explicit deny, we assume it's allowed
  210. // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
  211. // additionally, it's better to have false negatives here then false positives
  212. if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
  213. $this->logger->debug('Hiding non readable entry ' . $file->getName());
  214. continue;
  215. }
  216. }
  217. if ($hide) {
  218. $this->logger->debug('hiding hidden file ' . $file->getName());
  219. }
  220. if (!$hide) {
  221. yield $file;
  222. }
  223. } catch (ForbiddenException $e) {
  224. $this->logger->debug($e->getMessage(), ['exception' => $e]);
  225. } catch (NotFoundException $e) {
  226. $this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]);
  227. }
  228. }
  229. } catch (ConnectException $e) {
  230. $this->logger->error('Error while getting folder content', ['exception' => $e]);
  231. throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
  232. } catch (NotFoundException $e) {
  233. throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
  234. }
  235. }
  236. protected function formatInfo(IFileInfo $info): array {
  237. $result = [
  238. 'size' => $info->getSize(),
  239. 'mtime' => $info->getMTime(),
  240. ];
  241. if ($info->isDirectory()) {
  242. $result['type'] = 'dir';
  243. } else {
  244. $result['type'] = 'file';
  245. }
  246. return $result;
  247. }
  248. /**
  249. * Rename the files. If the source or the target is the root, the rename won't happen.
  250. *
  251. * @param string $source the old name of the path
  252. * @param string $target the new name of the path
  253. */
  254. public function rename(string $source, string $target, bool $retry = true): bool {
  255. if ($this->isRootDir($source) || $this->isRootDir($target)) {
  256. return false;
  257. }
  258. if ($this->caseSensitive === false
  259. && mb_strtolower($target) === mb_strtolower($source)
  260. ) {
  261. // Forbid changing case only on case-insensitive file system
  262. return false;
  263. }
  264. $absoluteSource = $this->buildPath($source);
  265. $absoluteTarget = $this->buildPath($target);
  266. try {
  267. $result = $this->share->rename($absoluteSource, $absoluteTarget);
  268. } catch (AlreadyExistsException $e) {
  269. if ($retry) {
  270. $this->remove($target);
  271. $result = $this->share->rename($absoluteSource, $absoluteTarget);
  272. } else {
  273. $this->logger->warning($e->getMessage(), ['exception' => $e]);
  274. return false;
  275. }
  276. } catch (InvalidArgumentException $e) {
  277. if ($retry) {
  278. $this->remove($target);
  279. $result = $this->share->rename($absoluteSource, $absoluteTarget);
  280. } else {
  281. $this->logger->warning($e->getMessage(), ['exception' => $e]);
  282. return false;
  283. }
  284. } catch (\Exception $e) {
  285. $this->logger->warning($e->getMessage(), ['exception' => $e]);
  286. return false;
  287. }
  288. unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
  289. return $result;
  290. }
  291. public function stat(string $path, bool $retry = true): array|false {
  292. try {
  293. $result = $this->formatInfo($this->getFileInfo($path));
  294. } catch (\OCP\Files\ForbiddenException $e) {
  295. return false;
  296. } catch (\OCP\Files\NotFoundException $e) {
  297. return false;
  298. } catch (TimedOutException $e) {
  299. if ($retry) {
  300. return $this->stat($path, false);
  301. } else {
  302. throw $e;
  303. }
  304. }
  305. if ($this->remoteIsShare() && $this->isRootDir($path)) {
  306. $result['mtime'] = $this->shareMTime();
  307. }
  308. return $result;
  309. }
  310. /**
  311. * get the best guess for the modification time of the share
  312. */
  313. private function shareMTime(): int {
  314. $highestMTime = 0;
  315. $files = $this->share->dir($this->root);
  316. foreach ($files as $fileInfo) {
  317. try {
  318. if ($fileInfo->getMTime() > $highestMTime) {
  319. $highestMTime = $fileInfo->getMTime();
  320. }
  321. } catch (NotFoundException $e) {
  322. // Ignore this, can happen on unavailable DFS shares
  323. } catch (ForbiddenException $e) {
  324. // Ignore this too - it's a symlink
  325. }
  326. }
  327. return $highestMTime;
  328. }
  329. /**
  330. * Check if the path is our root dir (not the smb one)
  331. */
  332. private function isRootDir(string $path): bool {
  333. return $path === '' || $path === '/' || $path === '.';
  334. }
  335. /**
  336. * Check if our root points to a smb share
  337. */
  338. private function remoteIsShare(): bool {
  339. return $this->share->getName() && (!$this->root || $this->root === '/');
  340. }
  341. public function unlink(string $path): bool {
  342. if ($this->isRootDir($path)) {
  343. return false;
  344. }
  345. try {
  346. if ($this->is_dir($path)) {
  347. return $this->rmdir($path);
  348. } else {
  349. $path = $this->buildPath($path);
  350. unset($this->statCache[$path]);
  351. $this->share->del($path);
  352. return true;
  353. }
  354. } catch (NotFoundException $e) {
  355. return false;
  356. } catch (ForbiddenException $e) {
  357. return false;
  358. } catch (ConnectException $e) {
  359. $this->logger->error('Error while deleting file', ['exception' => $e]);
  360. throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
  361. }
  362. }
  363. /**
  364. * check if a file or folder has been updated since $time
  365. */
  366. public function hasUpdated(string $path, int $time): bool {
  367. if (!$path and $this->root === '/') {
  368. // mtime doesn't work for shares, but giving the nature of the backend,
  369. // doing a full update is still just fast enough
  370. return true;
  371. } else {
  372. $actualTime = $this->filemtime($path);
  373. return $actualTime > $time;
  374. }
  375. }
  376. /**
  377. * @return resource|false
  378. */
  379. public function fopen(string $path, string $mode) {
  380. $fullPath = $this->buildPath($path);
  381. try {
  382. switch ($mode) {
  383. case 'r':
  384. case 'rb':
  385. if (!$this->file_exists($path)) {
  386. return false;
  387. }
  388. return $this->share->read($fullPath);
  389. case 'w':
  390. case 'wb':
  391. $source = $this->share->write($fullPath);
  392. return CallBackWrapper::wrap($source, null, null, function () use ($fullPath): void {
  393. unset($this->statCache[$fullPath]);
  394. });
  395. case 'a':
  396. case 'ab':
  397. case 'r+':
  398. case 'w+':
  399. case 'wb+':
  400. case 'a+':
  401. case 'x':
  402. case 'x+':
  403. case 'c':
  404. case 'c+':
  405. //emulate these
  406. if (strrpos($path, '.') !== false) {
  407. $ext = substr($path, strrpos($path, '.'));
  408. } else {
  409. $ext = '';
  410. }
  411. if ($this->file_exists($path)) {
  412. if (!$this->isUpdatable($path)) {
  413. return false;
  414. }
  415. $tmpFile = $this->getCachedFile($path);
  416. } else {
  417. if (!$this->isCreatable(dirname($path))) {
  418. return false;
  419. }
  420. $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
  421. }
  422. $source = fopen($tmpFile, $mode);
  423. $share = $this->share;
  424. return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share): void {
  425. unset($this->statCache[$fullPath]);
  426. $share->put($tmpFile, $fullPath);
  427. unlink($tmpFile);
  428. });
  429. }
  430. return false;
  431. } catch (NotFoundException $e) {
  432. return false;
  433. } catch (ForbiddenException $e) {
  434. return false;
  435. } catch (OutOfSpaceException $e) {
  436. throw new EntityTooLargeException('not enough available space to create file', 0, $e);
  437. } catch (ConnectException $e) {
  438. $this->logger->error('Error while opening file', ['exception' => $e]);
  439. throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
  440. }
  441. }
  442. public function rmdir(string $path): bool {
  443. if ($this->isRootDir($path)) {
  444. return false;
  445. }
  446. try {
  447. $this->statCache = new CappedMemoryCache();
  448. $content = $this->share->dir($this->buildPath($path));
  449. foreach ($content as $file) {
  450. if ($file->isDirectory()) {
  451. $this->rmdir($path . '/' . $file->getName());
  452. } else {
  453. $this->share->del($file->getPath());
  454. }
  455. }
  456. $this->share->rmdir($this->buildPath($path));
  457. return true;
  458. } catch (NotFoundException $e) {
  459. return false;
  460. } catch (ForbiddenException $e) {
  461. return false;
  462. } catch (ConnectException $e) {
  463. $this->logger->error('Error while removing folder', ['exception' => $e]);
  464. throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
  465. }
  466. }
  467. public function touch(string $path, ?int $mtime = null): bool {
  468. try {
  469. if (!$this->file_exists($path)) {
  470. $fh = $this->share->write($this->buildPath($path));
  471. fclose($fh);
  472. return true;
  473. }
  474. return false;
  475. } catch (OutOfSpaceException $e) {
  476. throw new EntityTooLargeException('not enough available space to create file', 0, $e);
  477. } catch (ConnectException $e) {
  478. $this->logger->error('Error while creating file', ['exception' => $e]);
  479. throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
  480. }
  481. }
  482. public function getMetaData(string $path): ?array {
  483. try {
  484. $fileInfo = $this->getFileInfo($path);
  485. } catch (\OCP\Files\NotFoundException $e) {
  486. return null;
  487. } catch (\OCP\Files\ForbiddenException $e) {
  488. return null;
  489. }
  490. return $this->getMetaDataFromFileInfo($fileInfo);
  491. }
  492. private function getMetaDataFromFileInfo(IFileInfo $fileInfo): array {
  493. $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
  494. if (
  495. !$fileInfo->isReadOnly() || $fileInfo->isDirectory()
  496. ) {
  497. $permissions += Constants::PERMISSION_DELETE;
  498. $permissions += Constants::PERMISSION_UPDATE;
  499. if ($fileInfo->isDirectory()) {
  500. $permissions += Constants::PERMISSION_CREATE;
  501. }
  502. }
  503. $data = [];
  504. if ($fileInfo->isDirectory()) {
  505. $data['mimetype'] = 'httpd/unix-directory';
  506. } else {
  507. $data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
  508. }
  509. $data['mtime'] = $fileInfo->getMTime();
  510. if ($fileInfo->isDirectory()) {
  511. $data['size'] = -1; //unknown
  512. } else {
  513. $data['size'] = $fileInfo->getSize();
  514. }
  515. $data['etag'] = $this->getETag($fileInfo->getPath());
  516. $data['storage_mtime'] = $data['mtime'];
  517. $data['permissions'] = $permissions;
  518. $data['name'] = $fileInfo->getName();
  519. return $data;
  520. }
  521. public function opendir(string $path) {
  522. try {
  523. $files = $this->getFolderContents($path);
  524. } catch (NotFoundException $e) {
  525. return false;
  526. } catch (NotPermittedException $e) {
  527. return false;
  528. }
  529. $names = array_map(function ($info) {
  530. /** @var IFileInfo $info */
  531. return $info->getName();
  532. }, iterator_to_array($files));
  533. return IteratorDirectory::wrap($names);
  534. }
  535. public function getDirectoryContent(string $directory): \Traversable {
  536. try {
  537. $files = $this->getFolderContents($directory);
  538. foreach ($files as $file) {
  539. yield $this->getMetaDataFromFileInfo($file);
  540. }
  541. } catch (NotFoundException $e) {
  542. return;
  543. } catch (NotPermittedException $e) {
  544. return;
  545. }
  546. }
  547. public function filetype(string $path): string|false {
  548. try {
  549. return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
  550. } catch (\OCP\Files\NotFoundException $e) {
  551. return false;
  552. } catch (\OCP\Files\ForbiddenException $e) {
  553. return false;
  554. }
  555. }
  556. public function mkdir(string $path): bool {
  557. $path = $this->buildPath($path);
  558. try {
  559. $this->share->mkdir($path);
  560. return true;
  561. } catch (ConnectException $e) {
  562. $this->logger->error('Error while creating folder', ['exception' => $e]);
  563. throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
  564. } catch (Exception $e) {
  565. return false;
  566. }
  567. }
  568. public function file_exists(string $path): bool {
  569. try {
  570. // Case sensitive filesystem doesn't matter for root directory
  571. if ($this->caseSensitive === false && $path !== '') {
  572. $filename = basename($path);
  573. $siblings = $this->getDirectoryContent(dirname($this->buildPath($path)));
  574. foreach ($siblings as $sibling) {
  575. if ($sibling['name'] === $filename) {
  576. return true;
  577. }
  578. }
  579. return false;
  580. }
  581. $this->getFileInfo($path);
  582. return true;
  583. } catch (\OCP\Files\NotFoundException $e) {
  584. return false;
  585. } catch (\OCP\Files\ForbiddenException $e) {
  586. return false;
  587. } catch (ConnectException $e) {
  588. throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
  589. }
  590. }
  591. public function isReadable(string $path): bool {
  592. try {
  593. $info = $this->getFileInfo($path);
  594. return $this->showHidden || !$info->isHidden();
  595. } catch (\OCP\Files\NotFoundException $e) {
  596. return false;
  597. } catch (\OCP\Files\ForbiddenException $e) {
  598. return false;
  599. }
  600. }
  601. public function isUpdatable(string $path): bool {
  602. try {
  603. $info = $this->getFileInfo($path);
  604. // following windows behaviour for read-only folders: they can be written into
  605. // (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
  606. return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
  607. } catch (\OCP\Files\NotFoundException $e) {
  608. return false;
  609. } catch (\OCP\Files\ForbiddenException $e) {
  610. return false;
  611. }
  612. }
  613. public function isDeletable(string $path): bool {
  614. try {
  615. $info = $this->getFileInfo($path);
  616. return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
  617. } catch (\OCP\Files\NotFoundException $e) {
  618. return false;
  619. } catch (\OCP\Files\ForbiddenException $e) {
  620. return false;
  621. }
  622. }
  623. /**
  624. * check if smbclient is installed
  625. */
  626. public static function checkDependencies(): array|bool {
  627. $system = \OCP\Server::get(SystemBridge::class);
  628. return Server::available($system) || NativeServer::available($system) ?: ['smbclient'];
  629. }
  630. public function test(): bool {
  631. try {
  632. return parent::test();
  633. } catch (StorageAuthException $e) {
  634. return false;
  635. } catch (ForbiddenException $e) {
  636. return false;
  637. } catch (Exception $e) {
  638. $this->logger->error($e->getMessage(), ['exception' => $e]);
  639. return false;
  640. }
  641. }
  642. public function listen(string $path, callable $callback): void {
  643. $this->notify($path)->listen(function (IChange $change) use ($callback) {
  644. if ($change instanceof IRenameChange) {
  645. return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
  646. } else {
  647. return $callback($change->getType(), $change->getPath());
  648. }
  649. });
  650. }
  651. public function notify(string $path): SMBNotifyHandler {
  652. $path = '/' . ltrim($path, '/');
  653. $shareNotifyHandler = $this->share->notify($this->buildPath($path));
  654. return new SMBNotifyHandler($shareNotifyHandler, $this->root);
  655. }
  656. }