Swift.php 16 KB

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