Swift.php 14 KB

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