Swift.php 16 KB

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