Swift.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-only
  7. */
  8. namespace OCA\Files_External\Lib\Storage;
  9. use GuzzleHttp\Psr7\Uri;
  10. use Icewind\Streams\CallbackWrapper;
  11. use Icewind\Streams\IteratorDirectory;
  12. use OC\Files\Filesystem;
  13. use OC\Files\ObjectStore\SwiftFactory;
  14. use OC\Files\Storage\Common;
  15. use OCP\Cache\CappedMemoryCache;
  16. use OCP\Files\IMimeTypeDetector;
  17. use OCP\Files\StorageAuthException;
  18. use OCP\Files\StorageBadConfigException;
  19. use OCP\Files\StorageNotAvailableException;
  20. use OCP\ICache;
  21. use OpenStack\Common\Error\BadResponseError;
  22. use OpenStack\ObjectStore\v1\Models\Container;
  23. use OpenStack\ObjectStore\v1\Models\StorageObject;
  24. use Psr\Log\LoggerInterface;
  25. class Swift extends Common {
  26. /** @var SwiftFactory */
  27. private $connectionFactory;
  28. /**
  29. * @var Container
  30. */
  31. private $container;
  32. /**
  33. * @var string
  34. */
  35. private $bucket;
  36. /**
  37. * Connection parameters
  38. *
  39. * @var array
  40. */
  41. private $params;
  42. /** @var string */
  43. private $id;
  44. /** @var \OC\Files\ObjectStore\Swift */
  45. private $objectStore;
  46. /** @var IMimeTypeDetector */
  47. private $mimeDetector;
  48. /**
  49. * Key value cache mapping path to data object. Maps path to
  50. * \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject for existing
  51. * paths and path to false for not existing paths.
  52. *
  53. * @var ICache
  54. */
  55. private $objectCache;
  56. private function normalizePath(string $path): string {
  57. $path = trim($path, '/');
  58. if (!$path) {
  59. $path = '.';
  60. }
  61. $path = str_replace('#', '%23', $path);
  62. return $path;
  63. }
  64. public const SUBCONTAINER_FILE = '.subcontainers';
  65. /**
  66. * Fetches an object from the API.
  67. * If the object is cached already or a
  68. * failed "doesn't exist" response was cached,
  69. * that one will be returned.
  70. *
  71. * @return StorageObject|false object
  72. * or false if the object did not exist
  73. * @throws StorageAuthException
  74. * @throws StorageNotAvailableException
  75. */
  76. private function fetchObject(string $path): StorageObject|false {
  77. $cached = $this->objectCache->get($path);
  78. if ($cached !== null) {
  79. // might be "false" if object did not exist from last check
  80. return $cached;
  81. }
  82. try {
  83. $object = $this->getContainer()->getObject($path);
  84. $object->retrieve();
  85. $this->objectCache->set($path, $object);
  86. return $object;
  87. } catch (BadResponseError $e) {
  88. // Expected response is "404 Not Found", so only log if it isn't
  89. if ($e->getResponse()->getStatusCode() !== 404) {
  90. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  91. 'exception' => $e,
  92. 'app' => 'files_external',
  93. ]);
  94. }
  95. $this->objectCache->set($path, false);
  96. return false;
  97. }
  98. }
  99. /**
  100. * Returns whether the given path exists.
  101. *
  102. * @return bool true if the object exist, false otherwise
  103. * @throws StorageAuthException
  104. * @throws StorageNotAvailableException
  105. */
  106. private function doesObjectExist(string $path): bool {
  107. return $this->fetchObject($path) !== false;
  108. }
  109. public function __construct(array $parameters) {
  110. if ((empty($parameters['key']) and empty($parameters['password']))
  111. or (empty($parameters['user']) && empty($parameters['userid'])) or empty($parameters['bucket'])
  112. or empty($parameters['region'])
  113. ) {
  114. throw new StorageBadConfigException('API Key or password, Login, Bucket and Region have to be configured.');
  115. }
  116. $user = $parameters['user'];
  117. $this->id = 'swift::' . $user . md5($parameters['bucket']);
  118. $bucketUrl = new Uri($parameters['bucket']);
  119. if ($bucketUrl->getHost()) {
  120. $parameters['bucket'] = basename($bucketUrl->getPath());
  121. $parameters['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath()));
  122. }
  123. if (empty($parameters['url'])) {
  124. $parameters['url'] = 'https://identity.api.rackspacecloud.com/v2.0/';
  125. }
  126. if (empty($parameters['service_name'])) {
  127. $parameters['service_name'] = 'cloudFiles';
  128. }
  129. $parameters['autocreate'] = true;
  130. if (isset($parameters['domain'])) {
  131. $parameters['user'] = [
  132. 'name' => $parameters['user'],
  133. 'password' => $parameters['password'],
  134. 'domain' => [
  135. 'name' => $parameters['domain'],
  136. ]
  137. ];
  138. }
  139. $this->params = $parameters;
  140. // FIXME: private class...
  141. $this->objectCache = new CappedMemoryCache();
  142. $this->connectionFactory = new SwiftFactory(
  143. \OC::$server->getMemCacheFactory()->createDistributed('swift/'),
  144. $this->params,
  145. \OC::$server->get(LoggerInterface::class)
  146. );
  147. $this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory);
  148. $this->bucket = $parameters['bucket'];
  149. $this->mimeDetector = \OC::$server->get(IMimeTypeDetector::class);
  150. }
  151. public function mkdir(string $path): bool {
  152. $path = $this->normalizePath($path);
  153. if ($this->is_dir($path)) {
  154. return false;
  155. }
  156. if ($path !== '.') {
  157. $path .= '/';
  158. }
  159. try {
  160. $this->getContainer()->createObject([
  161. 'name' => $path,
  162. 'content' => '',
  163. 'headers' => ['content-type' => 'httpd/unix-directory']
  164. ]);
  165. // invalidate so that the next access gets the real object
  166. // with all properties
  167. $this->objectCache->remove($path);
  168. } catch (BadResponseError $e) {
  169. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  170. 'exception' => $e,
  171. 'app' => 'files_external',
  172. ]);
  173. return false;
  174. }
  175. return true;
  176. }
  177. public function file_exists(string $path): bool {
  178. $path = $this->normalizePath($path);
  179. if ($path !== '.' && $this->is_dir($path)) {
  180. $path .= '/';
  181. }
  182. return $this->doesObjectExist($path);
  183. }
  184. public function rmdir(string $path): bool {
  185. $path = $this->normalizePath($path);
  186. if (!$this->is_dir($path) || !$this->isDeletable($path)) {
  187. return false;
  188. }
  189. $dh = $this->opendir($path);
  190. while (($file = readdir($dh)) !== false) {
  191. if (Filesystem::isIgnoredDir($file)) {
  192. continue;
  193. }
  194. if ($this->is_dir($path . '/' . $file)) {
  195. $this->rmdir($path . '/' . $file);
  196. } else {
  197. $this->unlink($path . '/' . $file);
  198. }
  199. }
  200. try {
  201. $this->objectStore->deleteObject($path . '/');
  202. $this->objectCache->remove($path . '/');
  203. } catch (BadResponseError $e) {
  204. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  205. 'exception' => $e,
  206. 'app' => 'files_external',
  207. ]);
  208. return false;
  209. }
  210. return true;
  211. }
  212. public function opendir(string $path) {
  213. $path = $this->normalizePath($path);
  214. if ($path === '.') {
  215. $path = '';
  216. } else {
  217. $path .= '/';
  218. }
  219. // $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of #
  220. try {
  221. $files = [];
  222. $objects = $this->getContainer()->listObjects([
  223. 'prefix' => $path,
  224. 'delimiter' => '/'
  225. ]);
  226. /** @var StorageObject $object */
  227. foreach ($objects as $object) {
  228. $file = basename($object->name);
  229. if ($file !== basename($path) && $file !== '.') {
  230. $files[] = $file;
  231. }
  232. }
  233. return IteratorDirectory::wrap($files);
  234. } catch (\Exception $e) {
  235. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  236. 'exception' => $e,
  237. 'app' => 'files_external',
  238. ]);
  239. return false;
  240. }
  241. }
  242. public function stat(string $path): array|false {
  243. $path = $this->normalizePath($path);
  244. if ($path === '.') {
  245. $path = '';
  246. } elseif ($this->is_dir($path)) {
  247. $path .= '/';
  248. }
  249. try {
  250. $object = $this->fetchObject($path);
  251. if (!$object) {
  252. return false;
  253. }
  254. } catch (BadResponseError $e) {
  255. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  256. 'exception' => $e,
  257. 'app' => 'files_external',
  258. ]);
  259. return false;
  260. }
  261. $dateTime = $object->lastModified ? \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified) : false;
  262. $mtime = $dateTime ? $dateTime->getTimestamp() : null;
  263. $objectMetadata = $object->getMetadata();
  264. if (isset($objectMetadata['timestamp'])) {
  265. $mtime = $objectMetadata['timestamp'];
  266. }
  267. if (!empty($mtime)) {
  268. $mtime = floor($mtime);
  269. }
  270. $stat = [];
  271. $stat['size'] = (int)$object->contentLength;
  272. $stat['mtime'] = $mtime;
  273. $stat['atime'] = time();
  274. return $stat;
  275. }
  276. public function filetype(string $path) {
  277. $path = $this->normalizePath($path);
  278. if ($path !== '.' && $this->doesObjectExist($path)) {
  279. return 'file';
  280. }
  281. if ($path !== '.') {
  282. $path .= '/';
  283. }
  284. if ($this->doesObjectExist($path)) {
  285. return 'dir';
  286. }
  287. }
  288. public function unlink(string $path): bool {
  289. $path = $this->normalizePath($path);
  290. if ($this->is_dir($path)) {
  291. return $this->rmdir($path);
  292. }
  293. try {
  294. $this->objectStore->deleteObject($path);
  295. $this->objectCache->remove($path);
  296. $this->objectCache->remove($path . '/');
  297. } catch (BadResponseError $e) {
  298. if ($e->getResponse()->getStatusCode() !== 404) {
  299. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  300. 'exception' => $e,
  301. 'app' => 'files_external',
  302. ]);
  303. throw $e;
  304. }
  305. }
  306. return true;
  307. }
  308. public function fopen(string $path, string $mode) {
  309. $path = $this->normalizePath($path);
  310. switch ($mode) {
  311. case 'a':
  312. case 'ab':
  313. case 'a+':
  314. return false;
  315. case 'r':
  316. case 'rb':
  317. try {
  318. return $this->objectStore->readObject($path);
  319. } catch (BadResponseError $e) {
  320. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  321. 'exception' => $e,
  322. 'app' => 'files_external',
  323. ]);
  324. return false;
  325. }
  326. case 'w':
  327. case 'wb':
  328. case 'r+':
  329. case 'w+':
  330. case 'wb+':
  331. case 'x':
  332. case 'x+':
  333. case 'c':
  334. case 'c+':
  335. if (strrpos($path, '.') !== false) {
  336. $ext = substr($path, strrpos($path, '.'));
  337. } else {
  338. $ext = '';
  339. }
  340. $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
  341. // Fetch existing file if required
  342. if ($mode[0] !== 'w' && $this->file_exists($path)) {
  343. if ($mode[0] === 'x') {
  344. // File cannot already exist
  345. return false;
  346. }
  347. $source = $this->fopen($path, 'r');
  348. file_put_contents($tmpFile, $source);
  349. }
  350. $handle = fopen($tmpFile, $mode);
  351. return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void {
  352. $this->writeBack($tmpFile, $path);
  353. });
  354. }
  355. }
  356. public function touch(string $path, ?int $mtime = null): bool {
  357. $path = $this->normalizePath($path);
  358. if (is_null($mtime)) {
  359. $mtime = time();
  360. }
  361. $metadata = ['timestamp' => (string)$mtime];
  362. if ($this->file_exists($path)) {
  363. if ($this->is_dir($path) && $path !== '.') {
  364. $path .= '/';
  365. }
  366. $object = $this->fetchObject($path);
  367. if ($object->mergeMetadata($metadata)) {
  368. // invalidate target object to force repopulation on fetch
  369. $this->objectCache->remove($path);
  370. }
  371. return true;
  372. } else {
  373. $mimeType = $this->mimeDetector->detectPath($path);
  374. $this->getContainer()->createObject([
  375. 'name' => $path,
  376. 'content' => '',
  377. 'headers' => ['content-type' => 'httpd/unix-directory']
  378. ]);
  379. // invalidate target object to force repopulation on fetch
  380. $this->objectCache->remove($path);
  381. return true;
  382. }
  383. }
  384. public function copy(string $source, string $target): bool {
  385. $source = $this->normalizePath($source);
  386. $target = $this->normalizePath($target);
  387. $fileType = $this->filetype($source);
  388. if ($fileType) {
  389. // make way
  390. $this->unlink($target);
  391. }
  392. if ($fileType === 'file') {
  393. try {
  394. $sourceObject = $this->fetchObject($source);
  395. $sourceObject->copy([
  396. 'destination' => $this->bucket . '/' . $target
  397. ]);
  398. // invalidate target object to force repopulation on fetch
  399. $this->objectCache->remove($target);
  400. $this->objectCache->remove($target . '/');
  401. } catch (BadResponseError $e) {
  402. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  403. 'exception' => $e,
  404. 'app' => 'files_external',
  405. ]);
  406. return false;
  407. }
  408. } elseif ($fileType === 'dir') {
  409. try {
  410. $sourceObject = $this->fetchObject($source . '/');
  411. $sourceObject->copy([
  412. 'destination' => $this->bucket . '/' . $target . '/'
  413. ]);
  414. // invalidate target object to force repopulation on fetch
  415. $this->objectCache->remove($target);
  416. $this->objectCache->remove($target . '/');
  417. } catch (BadResponseError $e) {
  418. \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [
  419. 'exception' => $e,
  420. 'app' => 'files_external',
  421. ]);
  422. return false;
  423. }
  424. $dh = $this->opendir($source);
  425. while (($file = readdir($dh)) !== false) {
  426. if (Filesystem::isIgnoredDir($file)) {
  427. continue;
  428. }
  429. $source = $source . '/' . $file;
  430. $target = $target . '/' . $file;
  431. $this->copy($source, $target);
  432. }
  433. } else {
  434. //file does not exist
  435. return false;
  436. }
  437. return true;
  438. }
  439. public function rename(string $source, string $target): bool {
  440. $source = $this->normalizePath($source);
  441. $target = $this->normalizePath($target);
  442. $fileType = $this->filetype($source);
  443. if ($fileType === 'dir' || $fileType === 'file') {
  444. // copy
  445. if ($this->copy($source, $target) === false) {
  446. return false;
  447. }
  448. // cleanup
  449. if ($this->unlink($source) === false) {
  450. throw new \Exception('failed to remove original');
  451. $this->unlink($target);
  452. return false;
  453. }
  454. return true;
  455. }
  456. return false;
  457. }
  458. public function getId(): string {
  459. return $this->id;
  460. }
  461. /**
  462. * Returns the initialized object store container.
  463. *
  464. * @return Container
  465. * @throws StorageAuthException
  466. * @throws StorageNotAvailableException
  467. */
  468. public function getContainer(): Container {
  469. if (is_null($this->container)) {
  470. $this->container = $this->connectionFactory->getContainer();
  471. if (!$this->file_exists('.')) {
  472. $this->mkdir('.');
  473. }
  474. }
  475. return $this->container;
  476. }
  477. public function writeBack(string $tmpFile, string $path): void {
  478. $fileData = fopen($tmpFile, 'r');
  479. $this->objectStore->writeObject($path, $fileData, $this->mimeDetector->detectPath($path));
  480. // invalidate target object to force repopulation on fetch
  481. $this->objectCache->remove($path);
  482. unlink($tmpFile);
  483. }
  484. public function hasUpdated(string $path, int $time): bool {
  485. if ($this->is_file($path)) {
  486. return parent::hasUpdated($path, $time);
  487. }
  488. $path = $this->normalizePath($path);
  489. $dh = $this->opendir($path);
  490. $content = [];
  491. while (($file = readdir($dh)) !== false) {
  492. $content[] = $file;
  493. }
  494. if ($path === '.') {
  495. $path = '';
  496. }
  497. $cachedContent = $this->getCache()->getFolderContents($path);
  498. $cachedNames = array_map(function ($content) {
  499. return $content['name'];
  500. }, $cachedContent);
  501. sort($cachedNames);
  502. sort($content);
  503. return $cachedNames !== $content;
  504. }
  505. /**
  506. * check if curl is installed
  507. */
  508. public static function checkDependencies(): bool {
  509. return true;
  510. }
  511. }