Swift.php 16 KB

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